[pre]    当进行XML数据转换的时候,我们经常会碰到XML数据文件中含有重复的元素。在这封技
术邮件中,我们将讨论一种解决该问题的方法。
问题:

让我们先来看一下具体的问题描述。假使有如下的一个XML数据文件,它包含了如下的内容:[/pre]
<Order>
<Item number= "1" >
<SKU>12345</SKU>
<Description>Standard Widget</Description>
</Item>
<Item number= "2" >
<SKU>54321</SKU>
<Description>Turbo Widget</Description>
</Item>
<Item number= "3" >
<SKU>12345</SKU>
<Description>Standard Widget</Description>
</Item>
</Order>

[pre]    在上面的XML数据文件中,每个Item元素都是单独显示的,请注意Item number1和Item number3
中的数据是一样的。但是,需求目标要求输出的XML数据文件中每个SKU元素个体不能重复,
并且要加上一个新的<Quantity>元素以显示每个Item元素数据的个数。需求目标的输出文件如下:[/pre]
<Order>
<Item>
<Quantity>2</Quantity>
<SKU>12345</SKU>
<Description>Standard Widget</Description>
</Item>
<Item>
<Quantity>1</Quantity>
<SKU>54321</SKU>
<Description>Turbo Widget</Description>
</Item>
</Order>

解决方法:

[pre]    提出的问题实际上有两个问题需要解决。第一,需要去除重复的SKU#12345元素个体(entry);
第二,需要提供一个新的<Quantity>元素以显示每个Item元素数据的个数。为了解决这些问
题,我们得使用一些XSLT的高级特性。
为了解决第一个问题,我们将使用XSLT的following操作。following和preceding操作
分别指示在一个for-each循环中的以后和以前节点(node)。following操作判断以后节点
如果和当前节点一样,则去除当前重复的节点。
为了解决第二个问题,我们需要得到每个Item元素的个数。幸运的是,XSLT提供计数(count)
功能。使用计数功能,我们可以对XML数据文件中出现的每个Item元素进行计数,并将这个
数值赋值给新建的<Quantity>元素。[/pre]
去除重复的元素个体:

[pre]    去除重复的元素个体需要一些小技巧。首先,将选择的节点放入一个for-each循环中,
但是这个循环中的select属性值需要一些小技巧。通常的做法,你会将所有的Item放入for-each
循环中,如下:[/pre]
<xsl: for -each select= "//Order/Item" >
. . .
</xsl: for -each>

[pre]    但是,我们需要每个SKU元素数据都唯一。为了能达到这种转换,我们得在select的属
性值中加入额外的信息。这个额外的信息将会告诉转换处理器只对以后节点和当前节点不同
的当前节点取值。举个小例子,如果第一个节点是A,下一个节点是A,那么就忽略第一个节
点;如果第一个节点是A,下一个节点是A,再下一个节点是B,那么循环之后对第二个节点
取值,第一个节点会被忽略。下面是该方法在XML的表达式:[/pre]
<xsl: for -each select= "//SKU[not(.=following::SKU)]" >
. . .
</xsl: for -each>

[pre]    在上面的XML表达式中,select的属性值决定了怎样循环取值选择的节点数据。它使得
我们只对以后节点和当前节点(用.表示)不同的当前节点取值。[/pre]
计数:

[pre]    需要对每个SKU元素进行计数并把它赋值给新建的<Quantity>元素,同样需要一些小技
巧。我们可以使用XSLT的计数(count)功能,但难题是告诉转换处理器需要对那些元素计数。
一个对所有SKU元素计数的简单例子如下:[/pre]
<xsl:value-of select= "count(//SKU)" />

[pre]    上面的XML表达式中,只是简单的对所有符合//SKU模式的元素进行计数。但是,我们需
要的是对满足特殊条件的SKU元素计数。技巧之处在于满足特殊条件的SKU元素值在每个for-each
循环中可以得到,并且是用点号(.)标识。那么解决计数问题的关键就是计数(count)功
能也需要使用点号(.)标识。所以,我们可以在每个for-each循环中,使用一个新变量,
如下所示:[/pre]
<xsl:variable name= "thesku" select= "." />

[pre]    接着,我们就可以使用计数(count)功能来对每个SKU元素进行计数了,如下所示:[/pre]
<Quantity><xsl:value-of select= "count(//SKU[.=$thesku])" /></Quantity>

完整的解决方法:

[pre]    现在,我们可以把以上所述的所有内容合并起来,得到该问题的完整解决方法。下面的
完整代码使用select属性来对SKU进行唯一性选择;而<Quantity>元素是使用计数XSLT的(count)
功能得到的;最后,<Description>元素的值是从原XML数据文件取值而来。[/pre]
<?xml version= "1.0" encoding= "UTF-8" ?>
<xsl:stylesheet version= "1.0" xmlns:xsl= "http://www.w3.org/1999/XSL/Transform" >
<xsl:template match= "/" >
<Order>
<xsl: for -each select= "//SKU[not(.=following::SKU)]" >
<xsl:variable name= "thesku" select= "." />
<Item>
<Quantity><xsl:value-of select= "count(//SKU[.=$thesku])" /></Quantity>
<SKU><xsl:value-of select= "." /></SKU>
<Description><xsl:value-of select= "../Description" /></Description>
</Item>
</xsl: for -each>
</Order>
</xsl:template>
</xsl:stylesheet>

[pre]编者注释:由于技术错误,在先前的XML技术邮件"Tokenizing strings with Xalan-Java"
(March 20, 2002)中,里面的代码有一个错误,特此更正,完整正确的代码如下:[/pre]
To use the tokenize function, we'll create a template that calls it, like the following:
<?xml version= "1.0" encoding= "UTF-8" ?>
<xsl:stylesheet version= "1.0"
xmlns:xsl= "http://www.w3.org/1999/XSL/Transform"
xmlns:xalan= "http://xml.apache.org/xalan" >
<xsl:template match= "/" >
<xsl: for -each select= "//CustomerAddress" >
<Address><xsl:value-of select= "Address1" /></Address>
<City><xsl:value-of select= "xalan:tokenize(Address2, ' ,')[1]" /></City>
<State><xsl:value-of select= "xalan:tokenize(Address2, ' ,')[2]" /></State>
<Zip><xsl:value-of select= "xalan:tokenize(Address2, ' ,')[3]" /></Zip>
</xsl: for -each>
</xsl:template>
</xsl:stylesheet>