Reputation: 17
Firstly, I suck at XSLT. Secondly, I know there are several articles on how to perform merges with XSLT, but I didn't find anything on my particular challenge.
I have 2 XML files. One is new Customer Information and the other is the Current/Previous information below. I need the resultant XML to merge all of the Customer/Addresses and add attributes (NoChange, Updated, Deleted, New) to the attribute of the final XML:
Current Customer Information.
<Customer>
<CustId>1</CustId>
<CustName>Acme</CustName>
<Addresses>
<Address>
<AddressesId>1</AddressesId>
<Street>123 Main</Street>
</Address>
<Address>
<AddressesId>2</AddressesId>
<Street>345 Main</Street</Street>
</Address>
<Address>
<AddressesId>4</AddressesId>
<Street>888 Goner St.</Street>
</Address>
</Addresses>
</Customer>
Updates Information.
<Customer>
<CustId>1</CustId>
<CustName>Acme</CustName>
<Addresses>
<Address>
<AddressesId>2</AddressesId>
<Street>999 Updated St.</Street>
</Address>
<Address>
<AddressesId>3</AddressesId>
<Street>3999 New St.</Street>
</Address>
</Addresses>
</Customer>
<Customer>
<CustId>1</CustId>
<CustName>Acme</CustName>
<Addresses>
<Address>
<Address status="NoChange">
<AddressesId>1</AddressesId>
<Street>123 Main</Street>
</Address>
<Address>
<Address status="Updated">
<AddressesId>2</AddressesId>
<Street>999 Updated St.</Street>
</Address>
<Address status="New">
<AddressesId>3</AddressesId>
<Street>3999 New St.</Street>
</Address>
<Address status="Deleted">
<AddressesId>4</AddressesId>
<Street>888 Goner St.</Street>
</Address>
</Addresses>
</Customer>
How can I do the merge I want?
Upvotes: 1
Views: 388
Reputation: 163595
I decided to try this as a use case for the new xsl:merge instruction in XSLT 3.0. Using the current Saxon implementation, the following gives the desired result:
<xsl:stylesheet version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs">
<xsl:mode on-no-match="shallow-copy"/>
<xsl:strip-space elements="*"/>
<xsl:output indent="yes"/>
<xsl:variable name="original" select="doc('merge018-current.xml')"/>
<xsl:variable name="updates" select="doc('merge018-updates.xml')"/>
<xsl:template name="main">
<xsl:apply-templates select="$original"/>
</xsl:template>
<xsl:template match="Addresses">
<Addresses>
<xsl:merge>
<xsl:merge-source for-each="$updates, $original"
select=".//Address">
<xsl:merge-key select="AddressesId"/>
</xsl:merge-source>
<xsl:merge-action>
<xsl:variable name="status" select="
if (count(current-group()) = 1)
then if (current-group()[1]/root(.) is $original) then 'Deleted' else 'New'
else if (deep-equal(current-group()[1], current-group()[2])) then 'NoChange' else 'Updated'"/>
<Address status="{$status}">
<xsl:copy-of select="current-group()[1]/(AddressesId, Street)"/>
</Address>
</xsl:merge-action>
</xsl:merge>
</Addresses>
</xsl:template>
</xsl:stylesheet>
I'm not suggesting this as a practical solution, just providing it for your interest. If you have any objection to this or something similar being published as a test case, please say so now.
Upvotes: 1
Reputation: 243579
I. This XSLT 1.0 transformation (a corresponding XSLT 2.0 solution is shorter and easier):
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="kAddrById" match="Address"
use="concat(../../CustId, '+', ../../CustName,
'+', AddressesId)"/>
<xsl:key name="kNodeByGenId" match="node()"
use="generate-id()"/>
<xsl:param name="pUpdatesPath" select=
"'file:///c:/temp/delete/CustomersUpdates.xml'"/>
<xsl:variable name="vUpdates" select=
"document($pUpdatesPath)"/>
<xsl:variable name="vmainDoc" select="/"/>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Addresses">
<Addresses>
<xsl:apply-templates select="Address | $vUpdates/*/*/*">
<xsl:sort select="AddressesId" data-type="number"/>
</xsl:apply-templates>
</Addresses>
</xsl:template>
<xsl:template match="Address">
<xsl:variable name="vIsThisUpdate" select=
"generate-id(/) = generate-id($vUpdates)"/>
<xsl:variable name="vOtherDoc" select=
"$vmainDoc[$vIsThisUpdate]
|
$vUpdates[not($vIsThisUpdate)]
"/>
<xsl:variable name="vCustId" select="../../CustId"/>
<xsl:variable name="vCustName" select="../../CustName"/>
<xsl:variable name="vAddrId" select="AddressesId"/>
<xsl:variable name="vOtherNodeId">
<xsl:for-each select="$vOtherDoc">
<xsl:value-of select=
"generate-id(key('kAddrById',
concat($vCustId,'+', $vCustName,
'+', $vAddrId)
)
)"/>
</xsl:for-each>
</xsl:variable>
<xsl:apply-templates mode="selected"
select="self::node()
[$vIsThisUpdate
or
(not($vIsThisUpdate) and not(string($vOtherNodeId)))
]">
<xsl:with-param name="pIsUpdating" select="$vIsThisUpdate"/>
<xsl:with-param name="pOtherDoc" select="$vOtherDoc"/>
<xsl:with-param name="pOtherNodeId"
select="string($vOtherNodeId)"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Address" mode="selected">
<xsl:param name="pIsUpdating"/>
<xsl:param name="pOtherDoc" select="/.."/>
<xsl:param name="pOtherNodeId"/>
<xsl:variable name="vStatus">
<xsl:choose>
<xsl:when test="$pIsUpdating and not($pOtherNodeId)">New</xsl:when>
<xsl:when test="$pIsUpdating">
<xsl:variable name="vOldStreet">
<xsl:for-each select="$pOtherDoc">
<xsl:value-of select=
"key('kNodeByGenId', $pOtherNodeId)/Street"/>
</xsl:for-each>
</xsl:variable>
<xsl:choose>
<xsl:when test=
"Street = string($vOldStreet)">NoChange</xsl:when>
<xsl:otherwise>Updated</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:otherwise>Deleted</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<Address>
<Address status="{$vStatus}"/>
<xsl:apply-templates/>
</Address>
</xsl:template>
</xsl:stylesheet>
when applied on the provided XML document:
<Customer>
<CustId>1</CustId>
<CustName>Acme</CustName>
<Addresses>
<Address>
<AddressesId>1</AddressesId>
<Street>123 Main</Street>
</Address>
<Address>
<AddressesId>2</AddressesId>
<Street>345 Main</Street>
</Address>
<Address>
<AddressesId>4</AddressesId>
<Street>888 Goner St.</Street>
</Address>
</Addresses>
</Customer>
and having at c:/temp/delete/CustomersUpdates.xml
this XML document (slightly changed from the provided in order to have the first address end up with status "NoChange"):
<Customer>
<CustId>1</CustId>
<CustName>Acme</CustName>
<Addresses>
<Address>
<AddressesId>1</AddressesId>
<Street>123 Main</Street>
</Address>
<Address>
<AddressesId>2</AddressesId>
<Street>999 Updated St.</Street>
</Address>
<Address>
<AddressesId>3</AddressesId>
<Street>3999 New St.</Street>
</Address>
</Addresses>
</Customer>
produces the wanted, correct result:
<Customer>
<CustId>1</CustId>
<CustName>Acme</CustName>
<Addresses>
<Address>
<Address status="NoChange"/>
<AddressesId>1</AddressesId>
<Street>123 Main</Street>
</Address>
<Address>
<Address status="Updated"/>
<AddressesId>2</AddressesId>
<Street>999 Updated St.</Street>
</Address>
<Address>
<Address status="New"/>
<AddressesId>3</AddressesId>
<Street>3999 New St.</Street>
</Address>
<Address>
<Address status="Deleted"/>
<AddressesId>4</AddressesId>
<Street>888 Goner St.</Street>
</Address>
</Addresses>
</Customer>
II. XSLT 2.0 solution:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="my:my" exclude-result-prefixes="my">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="kAddrById" match="Address"
use="concat(../../CustId, '+', ../../CustName,
'+', AddressesId)"/>
<xsl:param name="pUpdatesPath" select=
"'file:///c:/temp/delete/CustomersUpdates.xml'"/>
<xsl:variable name="vUpdates" select=
"document($pUpdatesPath)"/>
<xsl:variable name="vmainDoc" select="/"/>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Addresses">
<Addresses>
<xsl:apply-templates select="Address | $vUpdates/*/*/*">
<xsl:sort select="AddressesId" data-type="number"/>
</xsl:apply-templates>
</Addresses>
</xsl:template>
<xsl:template match="Address[root() is $vUpdates]">
<xsl:variable name="vOtherDoc" select="$vmainDoc"/>
<xsl:variable name="vOtherNode" select="my:OtherNode(., $vOtherDoc)"/>
<xsl:variable name="vStatus" select=
"concat('New'[not($vOtherNode)],
'NoChange'[$vOtherNode
and current()/Street eq $vOtherNode/Street],
'Updated'[$vOtherNode and current()/Street ne $vOtherNode/Street]
)"/>
<xsl:apply-templates select="self::node()" mode="selected">
<xsl:with-param name="pStatus" select="$vStatus"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Address[not(root() is $vUpdates)]">
<xsl:variable name="vOtherDoc" select="$vUpdates"/>
<xsl:variable name="vOtherNode" select="my:OtherNode(., $vOtherDoc)"/>
<xsl:apply-templates select="self::node()[not($vOtherNode)]" mode="selected">
<xsl:with-param name="pStatus" select="'Deleted'"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Address" mode="selected">
<xsl:param name="pStatus"/>
<Address>
<Address status="{$pStatus}"/>
<xsl:apply-templates/>
</Address>
</xsl:template>
<xsl:function name="my:OtherNode" as="element()?">
<xsl:param name="pThis" as="element()"/>
<xsl:param name="pOtherDoc" as="document-node()"/>
<xsl:sequence select=
"key('kAddrById',
concat($pThis/../../CustId,'+', $pThis/../../CustName,
'+', $pThis/AddressesId
),
$pOtherDoc
)"/>
</xsl:function>
</xsl:stylesheet>
when this transformation is applied on the same documents, the same correct result is produced.
Upvotes: 0