【问题标题】:How do I modify one JSON property using MarkLogic XSLT?如何使用 MarkLogic XSLT 修改一个 JSON 属性?
【发布时间】:2019-02-11 21:23:30
【问题描述】:

我有一些 JSON、一个 XPath 和一个值。我想用新值替换 XPath 指向的 JSON 属性中的现有值。我在想我可以用 XSLT 做到这一点,但我不太擅长 XSLT。这将在 XQuery 模块中。

对于 XML,我可以这样做:

let $content :=
  document {
    <class> 
      <student rollno = "393"> 
        <firstname>Dinkar</firstname> 
        <lastname>Kad</lastname> 
        <nickname>Dinkar</nickname> 
        <marks>85</marks> 
      </student> 
      <student rollno = "493"> 
        <firstname>Vaneet</firstname> 
        <lastname>Gupta</lastname> 
        <nickname>Vinni</nickname> 
        <marks>95</marks> 
      </student>
    </class>
  }
let $template := 
  <xsl:stylesheet version = "1.0" 
    xmlns:xsl = "http://www.w3.org/1999/XSL/Transform">
    <xsl:template match = "node()|@*"> 
      <xsl:copy>
        <xsl:apply-templates select="node()|@*"/>
      </xsl:copy>
    </xsl:template>
    <xsl:template match="student/marks">
      <foo>bar</foo>
    </xsl:template>
  </xsl:stylesheet>
return
    xdmp:xslt-eval($template, $content)

这会正确地将 class/student/marks 元素替换为 &lt;foo&gt;bar&lt;/foo&gt; 元素。

对于 JSON,我正在尝试这个:

let $stuff :=
  document {
    object-node {
      "SomeProperty": object-node {
        "LowProperty1":"some string", 
        "LowProperty2":"some string", 
        "LowProperty3": array-node { "some string 1", "some string 2"}
      }
    }
  }

let $target := xdmp:unpath("/EvenLowerProperty/LowestProperty1", (), $stuff)
return
  xdmp:xslt-eval(
    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        version="2.0"
        xmlns:json="http://marklogic.com/xdmp/json">
      <xsl:template match="node()"> 
        <xsl:copy>
          <xsl:apply-templates select="node()"/>
        </xsl:copy>
      </xsl:template>
      <xsl:template match="SomeProperty/LowProperty1">
        {
          map:entry("LowProperty1", "bar")
        }
      </xsl:template>
    </xsl:stylesheet>,
    $stuff
  )

我想这样结束:

{
  "SomeProperty": {
    "LowProperty1":"bar", 
    "LowProperty2":"some string", 
    "LowProperty3": [ "some string 1", "some string 2" ]
  }
}

相反,我得到的是原件的副本。我尝试了一些变化,但我没有更接近。我应该期望这行得通吗?

【问题讨论】:

    标签: json xslt marklogic


    【解决方案1】:

    我同意@wst 的观点,即xsl:copy 有点奇怪。至少在 ML9 中,它确实似乎做了一个复制,而不是递归到一个对象节点。尚未使用 ML10 进行测试。

    @mads-hansen 提到的xdmp:dialect 在这里绝对有用,尽管在复制时不必将整个树转换为 XML 表示法,最后再转换回 JSON。您还可以使用内部 json:object 类型,并在需要的地方进行浅层转换。

    这展示了如何用适用于 JSON 的东西替换通常的身份转换:

    xquery version "1.0-ml";
    
    let $stuff :=
      document {
        object-node {
          "SomeProperty": object-node {
            "LowProperty1":"some string", 
            "LowProperty2":"some string", 
            "LowProperty3": array-node { "some string 1", "some string 2"}
          }
        }
      }
    
    let $target := xdmp:unpath("/EvenLowerProperty/LowestProperty1", (), $stuff)
    return
      xdmp:xslt-eval(
        <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
            version="2.0" xdmp:dialect="1.0-ml"
            xmlns:json="http://marklogic.com/xdmp/json">
    
          <xsl:template match="object-node()">
            <xsl:variable name="this" select="json:object()"/>
            <xsl:for-each select="node()">
              <xsl:variable name="contents" as="item()*">
                <xsl:apply-templates select="."/>
              </xsl:variable>
              <xsl:sequence select="map:put($this, name(.), $contents)"/>
            </xsl:for-each>
            <xsl:sequence select="xdmp:to-json($this)"/>
          </xsl:template>
    
          <xsl:template match="array-node()">
            <xsl:variable name="contents" as="item()*">
              <xsl:apply-templates select="node()"/>
            </xsl:variable>
            <xsl:sequence select="json:to-array($contents)"/>
          </xsl:template>
    
          <xsl:template match="text()">
            <xsl:sequence select="."/>
          </xsl:template>
    
          <xsl:template match="SomeProperty/LowProperty1">
            <xsl:text>foo</xsl:text>
          </xsl:template>
        </xsl:stylesheet>,
        $stuff
      )
    

    HTH!

    【讨论】:

      【解决方案2】:

      我受到@MadsHansen 发现xdmp:dialect="1.0-ml" 选项的启发,以创建我的其他答案的更惯用的版本。使用此 XSLT,您可以保持使用 MarkLogic JSON XPath 扩展(即:match="SomeProperty/LowProperty1")创建模板的能力。

      这里的区别在于,不是一开始就转换为json:object XML 批发,而是最初维护原生 JSON 对象,仅在转换过程中转换为json:object。然后在最后,一切都被转换回本机。唯一的缺点是在模板内构造新 JSON 时需要使用 json:object XML,或者将原生构造函数包装在 xdmp:from-json() 中:

      <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
          xmlns:xs="http://www.w3.org/2001/XMLSchema" version="2.0"
          xmlns:json="http://marklogic.com/xdmp/json"
          xmlns:xdmp="http://marklogic.com/xdmp"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xdmp:dialect="1.0-ml">
      
          <!-- XML -->
          <xsl:template match="SomeProperty/LowProperty1">
            <xsl:text>bar</xsl:text>
          </xsl:template>     
      
          <!-- Native JSON syntax -->
          <xsl:template match="SomeProperty/LowProperty2">
            {xdmp:from-json(
              object-node { "foo" : "bar" }
            )}
          </xsl:template>  
      
          <!-- Conversion handling -->
      
          <xsl:template match="/">
            <xsl:variable name="result" as="node()">
              <xsl:apply-templates select="@*|node()"/>
            </xsl:variable>
            <xsl:choose>
              <xsl:when test="namespace-uri-from-QName($result/node-name(.)) = 'http://marklogic.com/xdmp/json'">
                <xsl:sequence select="xdmp:to-json(json:object($result))"/>
              </xsl:when>
              <xsl:otherwise>
                <xsl:sequence select="$result"/>
              </xsl:otherwise>
            </xsl:choose>
          </xsl:template>
      
          <!-- Identity templates below -->
      
          <xsl:template name="json:value">
            <xsl:variable name="result" as="node()">
              <xsl:apply-templates select="."/>
            </xsl:variable>        
            <json:value>                       
              <xsl:if test=". instance of number-node()">
                <xsl:attribute name="xsi:type">
                  <xsl:value-of select="xs:QName('xs:integer')"/>
                </xsl:attribute>
              </xsl:if>
              <xsl:sequence select="$result"/>
            </json:value>
          </xsl:template>
      
          <xsl:template match="object-node()"> 
            <json:object>
              <xsl:for-each select="node()">
                <json:entry key="{{ name(.) }}">
                  <xsl:call-template name="json:value"/>
                </json:entry>
              </xsl:for-each>
            </json:object>
          </xsl:template> 
      
          <xsl:template match="array-node()"> 
            <json:array>        
              <xsl:for-each select="node()">
                <xsl:call-template name="json:value"/>
              </xsl:for-each>
            </json:array>
          </xsl:template> 
      
          <xsl:template match="number-node()">
            <xsl:value-of select="."/>
          </xsl:template>
      
          <xsl:template match="node()|@*" priority="-1">
            <xsl:copy>
              <xsl:apply-templates select="node()|@*"/>
            </xsl:copy>
          </xsl:template>
      
      </xsl:stylesheet>
      

      另请注意,原生 JSON 语法仅在与 xdmp:xslt-eval 一起使用时才有效 - 在 XQuery 中评估原生语法并在评估 XSLT 之前转换为 json:object XML。

      【讨论】:

        【解决方案3】:

        如果您设置了xdmp:dialect="1.0-ml",那么除了能够使用 XPath 之外,您还可以为 JSON 节点类型使用模板匹配模式:object-node()array-node()number-node()boolean-node()null-node()并根据节点名称匹配模式,例如SomeProperty/LowProperty1

        不幸的是,xsl:copy 执行的深层复制使其难以转换,并且没有可用于这些 JSON 节点的 XSLT 节点构造函数。

        因此,将 JSON 转换为 XML、HTML 和文本非常简单,但是为了构建您想要的转换后的 JSON,您可以像 @wst 演示的那样与json:object 相互转换,或者您可以稍微作弊,只生成 JSON 格式的文本。

        使用一些匹配 JSON 节点并生成其 JSON 文本输出的基本模板,然后您可以添加自己的专用模板来更改 SomeProperty/LowProperty1 值:

        let $stuff :=
          document {
            object-node {
              "SomeProperty": object-node {
                "LowProperty1":"some string", 
                "LowProperty2":"some string", 
                "LowProperty3": array-node { "some string 1", "some string 2"}
              }
            }
          }
        
        let $target := xdmp:unpath("/EvenLowerProperty/LowestProperty1", (), $stuff)
        return
          xdmp:xslt-eval(
            <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0"
                xdmp:dialect="1.0-ml">
        
              <xsl:output method="text"/>
        
              <xsl:variable name="lcurly" select="'&#123;'"/>
              <xsl:variable name="rcurly" select="'&#125;'"/>
        
              <xsl:template match="node()">
              <xsl:apply-templates select="." mode="name"/>
                <xsl:apply-templates select="." mode="value"/>
              </xsl:template>
        
              <xsl:template match="array-node()/node()">
                <xsl:apply-templates select="." mode="value"/>
              </xsl:template>
        
              <xsl:template match="node()" mode="name">
                <xsl:if test="string(node-name(.))!=''">"<xsl:value-of select="node-name(.)"/>": </xsl:if>
              </xsl:template>
        
              <xsl:template match="text()" mode="value">
                <xsl:text>"</xsl:text><xsl:value-of select="."/><xsl:text>"</xsl:text>
                <xsl:if test="following-sibling::node()">, </xsl:if>
              </xsl:template>
        
              <xsl:template match="number-node() | boolean-node()" mode="value">
                <xsl:value-of select="."/>
                <xsl:if test="following-sibling::node()">, </xsl:if>
              </xsl:template>
        
              <xsl:template match="object-node()" mode="value">
                <xsl:value-of select="$lcurly"/>
                <xsl:apply-templates select="node()"/>
                <xsl:value-of select="$rcurly"/> 
                 <xsl:if test="following-sibling::node()">,</xsl:if>
              </xsl:template>
        
              <xsl:template match="array-node()/object-node()" mode="value">
                <xsl:value-of select="$lcurly"/>
                <xsl:apply-templates select="node()"/>
                <xsl:value-of select="$rcurly"/>
                <xsl:if test="following-sibling::node()">,</xsl:if>
              </xsl:template>
        
              <xsl:template match="array-node()" mode="value">
                <xsl:value-of select="'['"/>
                <xsl:apply-templates select="node()"/>
                <xsl:value-of select="']'"/> 
                <xsl:if test="following-sibling::node()">,</xsl:if>
              </xsl:template>
        
              <xsl:template match="null-node()" mode="value">
                <xsl:value-of select="'null'"/>
                <xsl:if test="following-sibling::node()">, </xsl:if>
              </xsl:template>
        
              <xsl:template match="SomeProperty/LowProperty1">
                <xsl:apply-templates select="." mode="name"/>
                <xsl:text>"bar"</xsl:text>
                <xsl:if test="following-sibling::node()">, </xsl:if>
              </xsl:template>
        
            </xsl:stylesheet>,
            $stuff
          )
        

        【讨论】:

          【解决方案4】:

          问题似乎是 MarkLogic 的 XSLT 处理器处理 JSON 扩展的程度不如其 XQuery 处理器。 &lt;xsl:copy&gt; 似乎被object-node() 短路,而不是仅复制上下文节点,其行为类似于&lt;xsl:copy-of&gt;,复制所有后代,这会阻止LowProperty1 模板(和任何其他模板)执行。您可以通过将&lt;xsl:message&gt; 添加到LowProperty1 模板来确认这一点,并查看该消息从未被记录。

          据我所知,没有惯用的方法可以从 XSLT 中复制 JSON 节点。因此,另一种方法是在转换之前和之后简单地转换为/从json:object 转换 - 当然这可以在运行 XSLT 之前在 XQuery 中完成(这可能更可取)。

          <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
              xmlns:xs="http://www.w3.org/2001/XMLSchema" version="2.0"
              xmlns:json="http://marklogic.com/xdmp/json"
              xmlns:xdmp="http://marklogic.com/xdmp">
          
             <xsl:template match="document-node()">
               <xsl:variable name="jsonxml" as="element()">
                 <temp><xsl:sequence select="xdmp:from-json(.)"/></temp>
               </xsl:variable>
               <xsl:variable name="result" as="element(json:object)">
                 <xsl:apply-templates select="$jsonxml/json:object"/>
               </xsl:variable>
               <xsl:sequence select="xdmp:to-json(json:object($result))"/>   
             </xsl:template>
          
              <xsl:template match="node()|@*"> 
                <xsl:copy>
                  <xsl:apply-templates select="node()|@*"/>
                </xsl:copy>
              </xsl:template>
          
              <xsl:template match="json:entry[@key='LowProperty1']/json:value">
                <xsl:copy>
                  <xsl:text>bar</xsl:text>
                </xsl:copy>
              </xsl:template>
          
          </xsl:stylesheet>
          

          【讨论】:

            【解决方案5】:

            除非 MarkLogic 做了一些我不知道的事情来扩展标准 XSLT 语义,否则这是行不通的。像SomeProperty/LowProperty1 这样的匹配模式不能用于寻址映射/数组树的部分。你可以匹配这样一棵树中的东西,但它不是很有用,因为匹配不能是上下文相关的:给定一个地图或数组,你无法找到它在哪里或如何到达那里。

            您可能会发现阅读我在 2016 年的 XML 布拉格论文中关于使用 XSLT 3.0 转换 JSON 的文章很有用:http://www.saxonica.com/papers/xmlprague-2016mhk.pdf

            使用 XSLT 模板匹配转换 XML 的标准方法不能很好地转换为 JSON,根本原因是用于表示 JSON 的映射/数组结构没有“节点标识”或向上导航(父指针) .在我论文中的示例中,我通常发现进行这种转换的最简单方法是将结构转换为 XML,转换 XML,然后再转换回来——尽管您可能会考虑其他方法。

            我一直在尝试设计一种高阶扩展功能,以使此类任务更容易。我认为我还没有理想的解决方案。

            【讨论】:

            • MarkLogic 并没有真正扩展 XSLT 的语义,而是将 JSON 解释为与 XML 非常相似的节点树。它不是元素和属性,而是由对象节点、数组节点、数字节点、空节点和文本组成。它使在 JSON 上使用 XPath 和 XSLT 成为可能,尽管有一些怪癖..
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-03-26
            • 1970-01-01
            • 1970-01-01
            • 2012-03-29
            • 2018-10-23
            • 1970-01-01
            相关资源
            最近更新 更多