从XPath表达式填充XML模板文件?

时间:2012-07-08 17:53:11

标签: java xml xslt xpath xslt-2.0

从XPath表达式的映射中填充(或生成)XML模板文件的最佳方法是什么?

要求是我们需要从模板开始(因为这可能包含未在XPath表达式中捕获的信息)。

例如,起始模板可能是:

<s11:Envelope xmlns:s11='http://schemas.xmlsoap.org/soap/envelope/'>
    <ns1:create xmlns:ns1='http://predic8.com/wsdl/material/ArticleService/1/'>
      <article xmlns:ns1='http://predic8.com/material/1/'>
        <name>?XXX?</name>
        <description>?XXX?</description>
        <price xmlns:ns1='http://predic8.com/common/1/'>
          <amount>?999.99?</amount>
          <currency xmlns:ns1='http://predic8.com/common/1/'>???</currency>
        </price>
        <id xmlns:ns1='http://predic8.com/material/1/'>???</id>
      </article>
    </ns1:create>
  </s11:Body>
</s11:Envelope>

然后我们提供了类似的东西:

expression: /create/article[1]/id                => 1
expression: /create/article[1]/description       => bar
expression: /create/article[1]/name[1]           => foo
expression: /create/article[1]/price[1]/amount   => 00.00
expression: /create/article[1]/price[1]/currency => USD
expression: /create/article[2]/id                => 2
expression: /create/article[2]/description       => some name
expression: /create/article[2]/name[1]           => some description
expression: /create/article[2]/price[1]/amount   => 00.01
expression: /create/article[2]/price[1]/currency => USD

然后我们应该生成:

<ns1:create xmlns:ns1='http://predic8.com/wsdl/material/ArticleService/1/'>
    <article xmlns:ns1='http://predic8.com/material/1/'>
        <name xmlns:ns1='http://predic8.com/material/1/'>foo</name>
        <description>bar</description>
        <price xmlns:ns1='http://predic8.com/common/1/'>
            <amount>00.00</amount>
            <currency xmlns:ns1='http://predic8.com/common/1/'>USD</currency>
        </price>
        <id xmlns:ns1='http://predic8.com/material/1/'>1</id>
    </article>
    <article xmlns:ns1='http://predic8.com/material/2/'>
        <name>some name</name>
        <description>some description</description>
        <price xmlns:ns1='http://predic8.com/common/2/'>
            <amount>00.01</amount>
            <currency xmlns:ns1='http://predic8.com/common/2/'>USD</currency>
        </price>
        <id xmlns:ns1='http://predic8.com/material/2/'>2</id>
    </article>
</ns1:create>

我是用Java实现的,但如果可能的话,我更喜欢基于XSLT的解决方案。

PS:这个问题与我最近提出的另一个question相反。

2 个答案:

答案 0 :(得分:1)

此转换从“表达式”创建一个具有所需结果结构的XML文档 - 它仍然可以将此结果转换为最终结果

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

 <xsl:variable name="vPop" as="element()*">
    <item path="/create/article[1]/id">1</item>
    <item path="/create/article[1]/description">bar</item>
    <item path="/create/article[1]/name[1]">foo</item>
    <item path="/create/article[1]/price[1]/amount">00.00</item>
    <item path="/create/article[1]/price[1]/currency">USD</item>
    <item path="/create/article[1]/price[2]/amount">11.11</item>
    <item path="/create/article[1]/price[2]/currency">AUD</item>
    <item path="/create/article[2]/id">2</item>
    <item path="/create/article[2]/description">some name</item>
    <item path="/create/article[2]/name[1]">some description</item>
    <item path="/create/article[2]/price[1]/amount">00.01</item>
    <item path="/create/article[2]/price[1]/currency">USD</item>
 </xsl:variable>

 <xsl:template match="/">
  <xsl:sequence select="my:subTree($vPop/@path/concat(.,'/',string(..)))"/>
 </xsl:template>

 <xsl:function name="my:subTree" as="node()*">
  <xsl:param name="pPaths" as="xs:string*"/>

  <xsl:for-each-group select="$pPaths"
    group-adjacent=
        "substring-before(substring-after(concat(., '/'), '/'), '/')">
    <xsl:if test="current-grouping-key()">
     <xsl:choose>
       <xsl:when test=
          "substring-after(current-group()[1], current-grouping-key())">
         <xsl:element name=
           "{substring-before(concat(current-grouping-key(), '['), '[')}">

          <xsl:sequence select=
            "my:subTree(for $s in current-group()
                         return
                            concat('/',substring-after(substring($s, 2),'/'))
                             )
            "/>
        </xsl:element>
       </xsl:when>
       <xsl:otherwise>
        <xsl:value-of select="current-grouping-key()"/>
       </xsl:otherwise>
     </xsl:choose>
     </xsl:if>
  </xsl:for-each-group>
 </xsl:function>
</xsl:stylesheet>

当此转换应用于任何XML文档(未使用)时,结果为

<create>
   <article>
      <id>1</id>
      <description>bar</description>
      <name>foo</name>
      <price>
         <amount>00.00</amount>
         <currency>USD</currency>
      </price>
      <price>
         <amount>11.11</amount>
         <currency>AUD</currency>
      </price>
   </article>
   <article>
      <id>2</id>
      <description>some name</description>
      <name>some description</name>
      <price>
         <amount>00.01</amount>
         <currency>USD</currency>
      </price>
   </article>
</create>

注意

  1. 您需要将您给出的“表达式”转换为此转换中使用的格式 - 这很简单直接。

  2. 在最终转换中,您需要“按原样”复制每个节点(使用标识规则),但顶级节点应在"http://predic8.com/wsdl/material/ArticleService/1/"命名空间中生成。请注意,“模板”中存在的其他名称空间不会被使用,可以安全地省略。

答案 1 :(得分:0)

此解决方案要求您稍微重新组织XPATH输入信息,并允许两步转换。第一个转换将编写样式表,它将在第二个转换中执行 - 因此客户端需要对XSLT引擎进行两次调用。如果这是一个问题,请告诉我们。

第一步

请将您的XPATH信息重新组织到XML文档中。这应该不难,甚至可以编写XSLT脚本来完成这项工作。

<paths>
 <rule>
  <match>article[1]/id[1]</match>
  <namespaces>
   <namespace prefix="ns1">http://predic8.com/wsdl/material/ArticleService/1/</namespace>
   <!-- The namespace node declares a namespace that is used in the match expression.
        There can be many of these. It is not required to define the s11: namespace,
        nor the ns1 namespace. -->
  </namespaces>
  <replacement>1</replacement>
 </rule> 
 <rule>
  <match>article[1]/description[1]</match>
  <namespaces/>
  <replacement>bar</replacement>
 </rule>
 ... etc ...
</paths>

解决方案约束

在上述规则文件中,我们受到限制,以便:

  1. 该匹配隐式加上'expression:/ create /'前缀。不要明确地说明。
  2. 所有匹配必须以文章[n]开头,其中n是某个序数。
  3. 我们不能有零规则。
  4. 您在匹配中使用的任何前缀,除了s11 =“http://schemas.xmlsoap.org/soap/envelope/”和ns1 =“http://predic8.com/wsdl/material/ArticleService/ 1 /”。 (注意:我不认为命名空间以'/'结尾 - 但不确定),在命名空间节点中定义。
  5. 以上是第一步转换的输入文档。将此文档应用于此样式表......

    <xsl:stylesheet version="2.0"
          xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
          xmlns:step2="http://www.w3.org/1999/XSL/Transform-step2"
          xmlns:s11="http://schemas.xmlsoap.org/soap/envelope/"                       
          xmlns:ns1="http://predic8.com/wsdl/material/ArticleService/1/"
      xmlns:xs="http://www.w3.org/2001/XMLSchema"
      exclude-result-prefixes='xsl'>
    <xsl:output method="xml" indent="yes" encoding="UTF-8" />
    <xsl:namespace-alias stylesheet-prefix="step2" result-prefix="xsl"/>
    
    <xsl:template match="/">
     <step2:stylesheet version="2.0">
     <step2:output method="xml" indent="yes" encoding="UTF-8" />
    
      <step2:variable name="replicated-template" as="element()*">
       <step2:apply-templates select="/" mode="replication" />
      </step2:variable>
    
      <step2:template match="@*|node()" mode="replication">
         <step2:copy>
            <step2:apply-templates select="@*|node()" mode="replication" />
         </step2:copy>
      </step2:template>
    
      <step2:template match="/s11:Envelope/s11:Body/ns1:create/article" mode="replication">
       <step2:variable name="replicant" select="." />  
        <step2:for-each select="for $i in 1 to
           {max(for $m in /paths/rule/match return
            xs:integer(substring-before(substring-after($m,'article['),']')))}
              return $i">
       <step2:for-each select="$replicant">
           <step2:copy>
            <step2:apply-templates select="@*|node()" mode="replication" />
           </step2:copy>
          </step2:for-each>   
         </step2:for-each>    
      </step2:template>
    
      <step2:template match="@*|node()">
       <step2:copy>
        <step2:apply-templates select="@*|node()"/>
       </step2:copy>
      </step2:template> 
    
      <step2:template match="/">
       <step2:apply-templates select="$replicated-template" />
      </step2:template>
    
      <xsl:apply-templates select="paths/rule" /> 
     </step2:stylesheet>
    </xsl:template>
    
    <xsl:template match="rule">
     <step2:template match="s11:Envelope/s11:Body/ns1:create/{match}">
      <xsl:for-each select="namespaces/namespace">
       <xsl:namespace name="{@prefix}" select="." />
      </xsl:for-each>
      <step2:copy>
       <step2:apply-templates select="@*"/>
       <step2:value-of select="'{replacement}'"/>
       <step2:apply-templates select="*"/>
      </step2:copy>
     </step2:template>
    </xsl:template>
    
    </xsl:stylesheet>
    

    第二步

    将肥皂信封文件作为输入文件应用于从第一步输出的样式表。结果是原始的soap文档,根据需要进行了更改。这是第二步样式表的示例,为了简化说明,仅考虑了第一个规则(/ create / article [1] / id =&gt; 1)。

    <?xml version="1.0" encoding="UTF-8"?>
    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                    xmlns:s11="http://schemas.xmlsoap.org/soap/envelope/"
                    version="2.0">
       <xsl:output method="xml" indent="yes" encoding="UTF-8"/>
       <xsl:template match="@*|node()">
          <xsl:copy>
             <xsl:apply-templates select="@*|node()"/>
          </xsl:copy>
       </xsl:template>
       <xsl:template xmlns:ns1="http://predic8.com/wsdl/material/ArticleService/1/"
                     match="/s11:Envelope/s11:Body/ns1:create[1]/article[1]/id[1]">
          <xsl:copy>
             <xsl:apply-templates select="@*"/>
             <xsl:value-of select="'1'"/>
             <xsl:apply-templates select="*"/>
          </xsl:copy>
       </xsl:template>
    </xsl:stylesheet>
    

    更多解决方案约束

    1. 模板文档必须至少包含一个/ s11:Envelope / s11:Body / ns1:create / article。根据规则的要求,只复制文章节点(深度)。除了它可以是任何结构。
    2. 模板文档不能包含s11的嵌套级别:Envelope / s11:Body / ns1:create node。
    3. 说明

      您会注意到XPATH表达式与模板的匹配条件相差不远。因此,编写一个将XPATH和替换值重新表示为模板规则的样式表并不太难。在编写样式表编写样式表时,xsl:namespace-alias使我们能够将“xsl:”作为指令消除歧义,并将“xsl:”作为预期输出消除歧义。当XSLT 3.0出现时,我们很安静,可能会将此算法缩减为一步,因为它将允许动态XPATH评估,这实际上是您问题的核心。但目前我们必须满足于两个步骤。

      第二个样式表是两阶段转换。第一阶段从文章级别复制模板,根据规则的需要多次复制。第二阶段解析此复制的模板,并应用动态规则替换XPATH指示的文本值。


      更新

      我原来的帖子错了。感谢Dimitre指出错误。请在上面找到更新的解决方案。

      后-以为

      如果两步解决过于复杂,并且您在wintel平台上运行,您可以考虑购买Saxon的商业版本。我相信商业版具有动态XPATH评估功能。我不能给你这样的解决方案,因为我没有商业版本。我想使用evaluate()函数的解决方案会简单得多。 XSLT对我来说只是一个爱好。但是,如果您将XSLT用于商业目的,那么价格就是合理的。