如何使用MarkLogic XSLT修改一个JSON属性?

时间:2018-09-06 15:00:20

标签: json xslt marklogic

我有一些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元素正确地替换为<foo>bar</foo>元素。

对于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" ]
  }
}

相反,我得到的是原件的副本。我尝试了一些变体,但距离还不很近。我应该期望这行得通吗?

5 个答案:

答案 0 :(得分:3)

问题似乎是MarkLogic的XSLT处理器对JSON扩展的处理程度与其XQuery处理器不同。 <xsl:copy>似乎被object-node()短路了,它不仅复制了上下文节点,而且还像<xsl:copy-of>一样,复制了所有后代,从而阻止了LowProperty1模板(以及任何其他模板)执行。您可以通过在<xsl:message>模板中添加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>

答案 1 :(得分:3)

如果设置了xdmp:dialect="1.0-ml",则可以对JSON节点类型使用模板匹配模式:object-node()array-node()number-node()boolean-node(),{ {1}}不仅可以使用XPath并根据节点名称(例如null-node())匹配模式。

不幸的是,SomeProperty/LowProperty1执行的深层复制使其难以转换,并且这些JSON节点没有XSLT节点构造函数。

因此,将JSON转换为XML,HTML和文本非常简单,但是为了构造所需的转换后的JSON,您将希望像@wst所示那样与xsl:copy进行转换,或者您可以作弊一点,只生成JSON文本。

使用一些与JSON节点匹配的基本模板并生成JSON文本输出,然后可以添加自己的专用模板以更改json:object值:

SomeProperty/LowProperty1

答案 2 :(得分:2)

除非MarkLogic做了我不知道的扩展标​​准XSLT语义的操作,否则这将行不通。 SomeProperty/LowProperty1之类的匹配模式不能用于解决地图/数组树的各个部分。您可以在这样的树中匹配事物,但是它并不是很有用,因为匹配不是上下文相关的:给定一个映射或数组,您将无法找到它的位置或到达那里的方式。

您可能会发现阅读有关使用XSLT 3.0转换JSON的XML Prague 2016论文很有用:http://www.saxonica.com/papers/xmlprague-2016mhk.pdf

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

我一直试图为高阶扩展功能设计一个使这种任务更容易的设计。我认为我还没有理想的解决方案。

答案 3 :(得分:2)

@MadsHansen的xdmp:dialect="1.0-ml"选项的发现启发了我,我创建了另一个答案的更惯用的版本。使用此XSLT,您可以使用MarkLogic JSON XPath扩展(即match="SomeProperty/LowProperty1")来创建模板。

此处的区别在于,本机JSON对象最初保持不变,而不是在一开始就转换为json:object XML批发,并且在转换期间仅转换为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求值之前,本机语法在XQuery中进行评估并转换为json:object XML。

答案 4 :(得分: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!