【发布时间】:2021-04-16 22:33:46
【问题描述】:
我需要将 XML 数据从一种结构转换为另一种结构。我需要基于源 XML 中的元数据来构建目标 XML。源 XML 具有固定的结构,但目标 XML 的结构需要根据源 XML 中的元数据动态构建(包括标签名称和数据分组)。
以下源 XML 提供了该结构的示例。源 XML 将始终使用 FMPXMLRESULT 结构。
XML
<?xml version="1.0" encoding="UTF-8" ?>
<FMPXMLRESULT xmlns="http://www.filemaker.com/fmpxmlresult">
<METADATA>
<FIELD EMPTYOK="YES" MAXREPEAT="1" NAME="ID" TYPE="NUMBER"/>
<FIELD EMPTYOK="YES" MAXREPEAT="1" NAME="Description" TYPE="TEXT"/>
<FIELD EMPTYOK="YES" MAXREPEAT="1" NAME="Customer" TYPE="TEXT"/>
<FIELD EMPTYOK="YES" MAXREPEAT="1" NAME="OrderItem::ProductName" TYPE="TEXT"/>
<FIELD EMPTYOK="YES" MAXREPEAT="1" NAME="OrderItem::UnitPrice" TYPE="NUMBER"/>
<FIELD EMPTYOK="YES" MAXREPEAT="1" NAME="OrderItem::Quantity" TYPE="NUMBER"/>
<FIELD EMPTYOK="YES" MAXREPEAT="1" NAME="OrderItem::TaxCode" TYPE="TEXT"/>
<FIELD EMPTYOK="YES" MAXREPEAT="1" NAME="OrderItem::Total" TYPE="NUMBER"/>
<FIELD EMPTYOK="YES" MAXREPEAT="1" NAME="OrderTaxCode::TaxCode" TYPE="TEXT"/>
<FIELD EMPTYOK="YES" MAXREPEAT="1" NAME="OrderTaxCode::TaxRate" TYPE="NUMBER"/>
</METADATA>
<RESULTSET FOUND="2">
<ROW MODID="1" RECORDID="1">
<COL><DATA>1</DATA></COL>
<COL><DATA>Order for first project</DATA></COL>
<COL><DATA>Customer No 1</DATA></COL>
<COL>
<DATA>Product A</DATA>
<DATA>Product B</DATA>
</COL>
<COL>
<DATA>10.50</DATA>
<DATA>12.10</DATA>
</COL>
<COL>
<DATA>2</DATA>
<DATA>1</DATA>
</COL>
<COL>
<DATA>VAT</DATA>
<DATA>VAT0</DATA>
</COL>
<COL>
<DATA>21</DATA>
<DATA>12.1</DATA>
</COL>
<COL>
<DATA>VAT</DATA>
<DATA>VAT0</DATA>
</COL>
<COL>
<DATA>0.2</DATA>
<DATA>0</DATA>
</COL>
</ROW>
<ROW MODID="1" RECORDID="2">
<COL><DATA>2</DATA></COL>
<COL><DATA>Order for second project</DATA></COL>
<COL><DATA>Customer No 2</DATA></COL>
<COL>
<DATA>Product 2A</DATA>
<DATA>Product 2B</DATA>
</COL>
<COL>
<DATA>1.50</DATA>
<DATA>345</DATA>
</COL>
<COL>
<DATA>17</DATA>
<DATA>2</DATA>
</COL>
<COL>
<DATA>VAT0</DATA>
<DATA>VAT</DATA>
</COL>
<COL>
<DATA>25.5</DATA>
<DATA>690</DATA>
</COL>
<COL>
<DATA>VAT</DATA>
<DATA>VAT0</DATA>
</COL>
<COL>
<DATA>0.2</DATA>
<DATA>0</DATA>
</COL>
</ROW>
</RESULTSET>
</FMPXMLRESULT>
鉴于上述 XML(包括元数据),目标 XML 格式如下。
XML
<?xml version="1.0" encoding="UTF-8"?>
<OrderBatch>
<Order>
<ID>1</ID>
<Description>Order for first project</Description>
<Customer>Customer No 1</Customer>
<OrderItem>
<ProductName>Product A</ProductName>
<UnitPrice>10.50</UnitPrice>
<Quantity>2</Quantity>
<TaxCode>VAT</TaxCode>
<Total>21</Total>
</OrderItem>
<OrderItem>
<ProductName>Product B</ProductName>
<UnitPrice>12.10</UnitPrice>
<Quantity>1</Quantity>
<TaxCode>VAT0</TaxCode>
<Total>12.1</Total>
</OrderItem>
<OrderTaxCode>
<TaxCode>VAT</TaxCode>
<TaxRate>0.2</TaxRate>
</OrderTaxCode>
<OrderTaxCode>
<TaxCode>VAT0</TaxCode>
<TaxRate>0</TaxRate>
</OrderTaxCode>
</Order>
<Order>
<ID>2</ID>
<Description>Order for second project</Description>
<Customer>Customer No 2</Customer>
<OrderItem>
<ProductName>Product 2A</ProductName>
<UnitPrice>1.50</UnitPrice>
<Quantity>17</Quantity>
<TaxCode>VAT0</TaxCode>
<Total>25.5</Total>
</OrderItem>
<OrderItem>
<ProductName>Product 2B</ProductName>
<UnitPrice>345</UnitPrice>
<Quantity>2</Quantity>
<TaxCode>VAT</TaxCode>
<Total>690</Total>
</OrderItem>
<OrderTaxCode>
<TaxCode>VAT</TaxCode>
<TaxRate>0.2</TaxRate>
</OrderTaxCode>
<OrderTaxCode>
<TaxCode>VAT0</TaxCode>
<TaxRate>0</TaxRate>
</OrderTaxCode>
</Order>
</OrderBatch>
源 XML 中的不同元数据会产生不同的目标 XML。一般规则如下
- 字段名称包含在 METADATA 中,数据包含在 RESULTSET 中
- RESULTSET 的每一行中的 COL 节点按位置对应于 METADATA 中的 FIELD 节点
- NAME 包含文本“::”的任何 RESULTSET/FIELD 都应被视为“分组”数据
- 分组数据的组名应与“::”前面的文本相同(该符号只会在字段名中出现一次)
- 分组数据 COL 节点可能包含 0、1 或多个 DATA 子节点
- 未分组的 FIELD 节点(即 NAME 不包含“::”)在 COL 节点中始终只有 1 个 DATA 子节点
- 分组数据字段将始终相邻(例如,组 ORDERITEM:: 中的所有字段在字段顺序中不会有来自其他组的字段)
- 事先不知道组名、字段名和字段顺序,可能会发生变化,XSLT 需要动态处理。上面的 XML 是需要处理的事情的一个很好的例子
- 我只能使用 XSLT 1.0
- FMPDSORESULT 是一项已弃用的技术,我无法使用它
两个主要症结是
- 按位置将 DATA 节点从 COL 节点中拉出并分配给它们自己的组
- 通过单独的元数据实现所需
我已经尝试了许多嵌套 for-each 循环的方法,以及组织模板的不同方式。我想知道是否可能创建一个内部数据结构可能是要走的路,但我也可能看错了问题?
这是迄今为止我想出的最好的,这是我最接近的,但还不够接近
XSLT 1.0
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fmp="http://www.filemaker.com/fmpxmlresult"
exclude-result-prefixes="fmp"
>
<xsl:output indent="yes"/>
<!-- the key indexes the METADATA fields by their position -->
<xsl:key
name="fieldList"
match="fmp:METADATA/fmp:FIELD"
use="count(preceding-sibling::fmp:FIELD) + 1"
/>
<!-- template for the data section of the FileMaker XML -->
<xsl:template match="/fmp:FMPXMLRESULT">
<OrderBatch>
<xsl:apply-templates select="fmp:RESULTSET/fmp:ROW" />
</OrderBatch>
</xsl:template>
<!-- template for each row -->
<xsl:template match="fmp:ROW">
<!-- for each row, create Order element and apply the relevant template for each column -->
<Order>
<xsl:apply-templates select="fmp:COL" />
</Order>
</xsl:template>
<!-- template for each column within each row -->
<xsl:template match="fmp:COL">
<!-- set $qualified with the name of the field - this will be qualified with the table occurrence if related -->
<xsl:variable name="qualified" select="string(key('fieldList', position())/@NAME)"/>
<!-- set $group with the name of the field's group -->
<xsl:variable name="group" select="substring-before($qualified, '::')"/>
<!-- set $name to a value for use as an XML element -->
<xsl:variable name="name">
<xsl:choose>
<!-- if the qualified field is related (contains "::") then remove the table occurrence name -->
<xsl:when test="contains($qualified, '::')">
<xsl:value-of select="substring-after($qualified, '::')"/>
</xsl:when>
<!-- if the qualified field is not related then just return the field name -->
<xsl:otherwise>
<xsl:value-of select="$qualified"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<!-- create the element with the field's name and use the data as the element's value -->
<xsl:choose>
<!-- related element - need to group -->
<xsl:when test="contains($qualified, '::')">
<!-- group each DATA element in turn -->
<!-- actually only need to run this on the first COL in a group - but I'll figure that out later -->
<xsl:for-each select="fmp:DATA">
<xsl:apply-templates select=".">
<xsl:with-param name="pGroup" select="$group" />
<xsl:with-param name="pName" select="$name" />
</xsl:apply-templates>
</xsl:for-each>
</xsl:when>
<!-- element is at top level so just create the field/value -->
<xsl:otherwise>
<xsl:element name="{$name}">
<xsl:value-of select="." />
</xsl:element>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- template for grouping DATA nodes across multiple COL nodes -->
<xsl:template match="fmp:DATA">
<xsl:param name = "pGroup" />
<xsl:param name = "pName" />
<xsl:element name="{$pGroup}">
<xsl:variable name="pos" select="position()" />
<xsl:apply-templates select="../../fmp:COL" mode="group">
<xsl:with-param name="pGroup" select="$pGroup" />
<xsl:with-param name="pName" select="$pName" />
<xsl:with-param name="pos" select="$pos" />
</xsl:apply-templates>
</xsl:element>
</xsl:template>
<!-- template for cycling through COL nodes and getting the DATA node if it belongs to the specified group -->
<xsl:template match="fmp:COL" mode="group">
<xsl:param name = "pGroup" />
<xsl:param name = "pName" />
<xsl:param name = "pos" /> <!-- this will help select the correct DATA node - not sure how to use it yet though -->
<xsl:variable name="qualified" select="string(key('fieldList', position())/@NAME)"/>
<xsl:variable name="colGroup" select="substring-before($qualified, '::')"/>
<xsl:if test="contains($qualified, '::') and $pGroup = $colGroup">
<xsl:element name="substring-after($qualified, '::')">
<xsl:value-of select="." />
</xsl:element>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
我知道这并不容易,也不是使用 XSLT 的正常方式(它通常会被编写为适合目标结构),但是我相信这是一个可以解决的问题,而且 XSLT 似乎能够远更复杂的任务。
非常欢迎任何有关如何解决此问题的帮助。非常感谢。
【问题讨论】:
标签: xml xslt-1.0 filemaker xslt-grouping