I want to change the ParentId of the child of the skipped element using xslt. My input file :
<?xml version="1.0" encoding="UTF-8"?>
<Hierarchy>
<Board>
<Name>President</Name>
<Id>ABCDE</Id>
<ParentId></ParentId>
<General>
<Name>President</Name>
<Description>Top level of the Hierarchy</Description>
<Template>LEVEL1</Template>
</General>
</Board>
<Board>
<Name>VP</Name>
<Id>EFGHI</Id>
<ParentId>ABCDE</ParentId>
<General>
<Name>VP</Name>
<Description>Below the President</Description>
<Template>LEVEL2</Template>
</General>
</Board>
<Board>
<Name>Department_Heads</Name>
<Id>JKLMN</Id>
<ParentId>EFGHI</ParentId>
<General>
<Name>Department_Heads</Name>
<Description>Reports to the VP</Description>
<Template>LEVEL3</Template>
</General>
</Board>
<Board>
<Name>Supervisors</Name>
<Id>OPQRS</Id>
<ParentId>JKLMN</ParentId>
<General>
<Name>Supervisors</Name>
<Description>Reports to the Reports to Dep Heads</Description>
<Template>LEVEL4</Template>
</General>
</Board>
<Board>
<Name>Employees</Name>
<Id>TUVWX</Id>
<ParentId>OPQRS</ParentId>
<General>
<Name>Employees</Name>
<Description>Reports to the Reports to Dep Heads</Description>
<Template>LEVEL5</Template>
</General>
</Board>
</Hierarchy>
Expected Output file :
<?xml version="1.0" encoding="UTF-8"?>
<Hierarchy>
<Board>
<Name>President</Name>
<Description>Top level of the Hierarchy</Description>
<Template>LEVEL1</Template>
<Id>ABCDE</Id>
<ParentId></ParentId>
<Board>
<Name>VP</Name>
<Description>Below the President</Description>
<Template>LEVEL2</Template>
<Id>EFGHI</Id>
<ParentId>ABCDE</ParentId>
<Board>
<Name>Supervisors</Name>
<Description>Reports to the Reports to Dep Heads</Description>
<Template>LEVEL4</Template>
<Id>OPQRS</Id>
<ParentId>EFGHI</ParentId>
<Board>
<Name>Employees</Name>
<Description>Reports to the Reports to Dep Heads</Description>
<Template>LEVEL5</Template>
<Id>TUVWX</Id>
<ParentId>OPQRS</ParentId>
</Board>
</Board>
</Board>
</Board>
</Hierarchy>
Now with the help of another question i raised, I am able to find a way to skip elements. Question link below :
Is it possible to skip a level in a Hierarchy using XSLT?
But If you observe in the expected output, The ParentId of Supervisors is changed to the Id of VP, thus skipping the element "Depeartment Heads" altogether. Is this possible? Can someone give a possible solution to this? Thanks in advance.
I don't see why you need the Board elements to continue to carry their parent's Id, after you have changed the structure to a proper hierarchical one.
Anyone receiving a result in the following format:
<Hierarchy>
<Board>
<Name>President</Name>
<Description>Top level of the Hierarchy</Description>
<Template>LEVEL1</Template>
<Id>ABCDE</Id>
<Board>
<Name>VP</Name>
<Description>Below the President</Description>
<Template>LEVEL2</Template>
<Id>EFGHI</Id>
<Board>
<Name>Supervisors</Name>
<Description>Reports to the Reports to Dep Heads</Description>
<Template>LEVEL4</Template>
<Id>OPQRS</Id>
<Board>
<Name>Employees</Name>
<Description>Reports to the Reports to Dep Heads</Description>
<Template>LEVEL5</Template>
<Id>TUVWX</Id>
</Board>
</Board>
</Board>
</Board>
</Hierarchy>
can easily see that the parent of the Board named Supervisors is the Board named VP, and its Id value of EFGHI is readily available using the expression parent::Id. Duplicating this value in a separate ParentId element is utterly redundant.
Still, if you want this unnecessary complication, you could do:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:key name="children" match="Board" use="ParentId" />
<xsl:template match="/Hierarchy">
<xsl:copy>
<xsl:apply-templates select="Board[General/Template='LEVEL1']"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Board">
<xsl:param name="parentId" select="ParentId"/>
<xsl:copy>
<xsl:copy-of select="General/*"/>
<xsl:copy-of select="Id"/>
<ParentId>
<xsl:value-of select="$parentId"/>
</ParentId>
<xsl:apply-templates select="key('children', Id)"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Board[General/Template='LEVEL3']">
<xsl:apply-templates select="key('children', Id)">
<xsl:with-param name="parentId" select="ParentId"/>
</xsl:apply-templates>
</xsl:template>
</xsl:stylesheet>
Note that this assumes you won't be skipping two or more consecutive levels.
Related
I have a question regarding bringing an attribute of an element to its parent or even grandparent. My XML file is as below :
<Hierarchy>
<Board>
<Id>ABCDE</Id>
<ParentId></ParentId>
<General>
<Name>President</Name>
<Description>Top level of the Hierarchy</Description>
<Template>LEVEL1</Template>
<LineManager>VP</LineManager>
</General>
</Board>
<Board>
<Id>EFGHI</Id>
<ParentId>ABCDE</ParentId>
<General>
<Name>VP</Name>
<Description>Below the President</Description>
<Template>LEVEL2</Template>
<LineManager>Department_Heads</LineManager>
</General>
</Board>
<Board>
<Id>JKLMN</Id>
<ParentId>EFGHI</ParentId>
<General>
<Name>Department_Heads</Name>
<Description>Reports to the VP</Description>
<Template>LEVEL3</Template>
<LineManager>Supervisors</LineManager>
</General>
</Board>
<Board>
<Id>OPQRS</Id>
<ParentId>JKLMN</ParentId>
<General>
<Name>Supervisors</Name>
<Description>Reports to the Reports to Dep Heads</Description>
<Template>LEVEL4</Template>
<LineManager>Employees</LineManager>
</General>
</Board>
<Board>
<Id>TUVWX</Id>
<ParentId>OPQRS</ParentId>
<General>
<Name>Employees</Name>
<Description>Reports to the Reports to Dep Heads</Description>
<Template>LEVEL5</Template>
<LineManager></LineManager>
</General>
</Board>
</Hierarchy>
How do I bring the "LineManager" from all levels directly at "VP". So I need to skip the "Supervisor" and "Department_Heads", directly pick the "LineManager" from all level in the hierarchy and place it at VP as an element. The expected output is below :
<?xml version="1.0" encoding="UTF-8"?>
<Hierarchy>
<Board>
<Name>President</Name>
<Description>Top level of the Hierarchy</Description>
<LineManager>VP</LineManager>
<Board>
<Name>VP</Name>
<Description>Below the President</Description>
<LineManager>Department_Heads</LineManager>
<LineManager>Supervisors</LineManager>
<LineManager>Employees</LineManager>
</Board>
</Board>
</Hierarchy>
So here, all the "LineManager" element from all levels below VP is placed at VP.
I tried something and I will post it below :
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<Hierarchy>
<xsl:for-each select="Hierarchy/Board">
<xsl:if test="General/Template='LEVEL1'">
<xsl:variable name="level1" select="Id"/>
<Board>
<Name><xsl:value-of select = "General/Name"/></Name>
<Description><xsl:value-of select = "General/Description"/></Description>
<LineManager><xsl:value-of select = "General/LineManager"/></LineManager>
<xsl:for-each select="//Board">
<xsl:if test="ParentId = $level1">
<xsl:if test="General/Template='LEVEL2'">
<xsl:variable name="level2" select="Id"/>
<Board>
<Name><xsl:value-of select = "General/Name"/></Name>
<Description><xsl:value-of select = "General/Description"/></Description>
<LineManager><xsl:value-of select = "General/LineManager"/></LineManager>
<xsl:for-each select="//Board">
<xsl:if test="ParentId = $level2">
<xsl:variable name="tagname1" select="General/LineManager"/>
<xsl:for-each select="General">
<xsl:if test="LineManager='Supervisors'">
<LineManager><xsl:value-of select="$tagname1"/></LineManager>
</xsl:if>
</xsl:for-each>
</xsl:if>
</xsl:for-each>
</Board>
</xsl:if>
</xsl:if>
</xsl:for-each>
</Board>
</xsl:if>
</xsl:for-each>
</Hierarchy>
</xsl:template>
</xsl:stylesheet>
The result of my test gives me only the element directly under VP which is the "Department_Heads". But I want all the "LineManager" elements from all levels to be placed at VP.
Please let me know if any further information is required.
I have the following:
<ns0:tXML>
<Message>
<Report>
<Page>
<PageID>01</PageID>
<PageDetail>
<PageName>11</PageName>
<Totals>
<Num>10</Num>
</Totals>
</PageDetail>
<PageDetail>
<PageName>11</PageName>
<Totals>
<Num>5</Num>
</Totals>
</PageDetail>
</Page>
<Page>
<PageID>02</PageID>
<PageDetail>
<PageName>12</PageName>
<Totals>
<Num>10</Num>
</Totals>
</PageDetail>
<PageDetail>
<PageName>12</PageName>
<Totals>
<Num>3</Num>
</Totals>
</PageDetail>
</Page>
</Report>
</Message>
</ns0:tXML>
I want to make the output so that PageDetails are combined for each Page as long as their PageName and PageID are the same, including summing the values of the combined.
Output Wanted:
<ns0:tXML>
<Message>
<Report>
<Page>
<PageID>01</PageID>
<PageDetail>
<PageName>11</PageName>
<Totals>
<Num>15</Num>
</Totals>
</PageDetail>
</Page>
<Page>
<PageID>02</PageID>
<PageDetail>
<PageName>12</PageName>
<Totals>
<Num>13</Num>
</Totals>
</PageDetail>
</Page>
</Report>
</Message>
</ns0:tXML>
How would I go about it? All efforts with using keys and playing with templates has led to cases where only one of the Pages got created, or it combined all the Pages no matter where they were on the xml, showing that I was likely trying to do an all apply to it rather than sticking to the current context.
Let's start from a little correction to your source. It should include
the namespace specification:
<ns0:tXML xmlns:ns0="urn.dummy.com">
otherwise there is reported the following error:
The prefix "ns0" for element "ns0:tXML" is not bound.
One of possible solutions is to use the following script:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ns0="urn.dummy.com">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="Pd" match="PageDetail" use="concat(../PageID, '|', PageName)"/>
<xsl:template match="Page">
<xsl:copy>
<xsl:copy-of select="PageID"/>
<xsl:for-each select="PageDetail[generate-id()=generate-id(key('Pd',
concat(../PageID,'|', PageName))[1])]">
<xsl:variable name="kk" select="concat(../PageID,'|', PageName)"/>
<xsl:copy>
<xsl:copy-of select="PageName"/>
<xsl:element name="Totals">
<xsl:element name="Num">
<xsl:value-of select="sum(key('Pd', $kk)/Totals/Num)"/>
</xsl:element>
</xsl:element>
</xsl:copy>
</xsl:for-each>
</xsl:copy>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy><xsl:apply-templates select="#*|node()"/></xsl:copy>
</xsl:template>
</xsl:transform>
For a working example, generating just your expected result,
see: http://xsltransform.net/93YRmgt
I have a large XML file to transform using XSLT to append the integer position of sibling node . I’m using XSLT3 streaming and accumulators. I did get desired output. However, my code looks so lengthy that I’m unable to simplify my code. I also need to group same sibling nodes as sibling nodes in the source xml is not grouped always. Could someone help me here please?
Requirement: Sibling nodes such as Positions, Payments etc.. need to be appended with their corresponding integer position such as <Locations1>, <Locations2>etc.<Payments1>,< Payments2> etc..
Now that I have declared two accumulators, each for each sibling nodes. However, my source XML has many sibling nodes.. I’m not sure if I need to use as many accumulators and template match as my sibling nodes.
Input XML
``
<?xml version="1.0" encoding="UTF-8"?>
<Members>
<Member>
<Name>
<fname>Fred</fname>
<id>1234</id>
</Name>
<Locations>
<name>Chicago</name>
<days>3</days>
<hours>24</hours>
</Locations>
<Locations>
<name>Chicago</name>
<days>3</days>
<hours>24</hours>
</Locations>
<Payments>
<amount>1000</amount>
<currency>USD</currency>
</Payments>
<Payments>
<amount>1000</amount>
<currency>USD</currency>
</Payments>
<Locations>
<name>New York</name>
<days>5</days>
<hours>40</hours>
</Locations>
<Locations>
<name>Boston</name>
<days>4</days>
<hours>32</hours>
</Locations>
</Member>
<Member>
<Name>
<fname>Jack</fname>
<id>4567</id>
</Name>
<Locations>
<name>New York</name>
<days>5</days>
<hours>30</hours>
</Locations>
<Locations>
<name>Chicago</name>
<days>3</days>
<hours>24</hours>
</Locations>
<Payments>
<amount>1500</amount>
<currency>USD</currency>
</Payments>
<Payments>
<amount>1800</amount>
<currency>USD</currency>
</Payments>
</Member>
</Members>
``
Expected Output
``
<?xml version="1.0" encoding="UTF-8"?>
<Members>
<Member>
<Name>
<fname>Fred</fname>
<id>1234</id>
</Name>
<Locations_1>
<name>Chicago</name>
<days>3</days>
<hours>24</hours>
</Locations_1>
<Locations_2>
<name>Chicago</name>
<days>3</days>
<hours>24</hours>
</Locations_2>
<Locations_3>
<name>New York</name>
<days>5</days>
<hours>40</hours>
</Locations_3>
<Locations_4>
<name>Boston</name>
<days>4</days>
<hours>32</hours>
</Locations_4>
<Payments_1>
<amount>1000</amount>
<currency>USD</currency>
</Payments_1>
<Payments_2>
<amount>1000</amount>
<currency>USD</currency>
</Payments_2>
</Member>
<Member>
<Name>
<fname>Jack</fname>
<id>4567</id>
</Name>
<Locations_1>
<name>New York</name>
<days>5</days>
<hours>30</hours>
</Locations_1>
<Locations_2>
<name>Chicago</name>
<days>3</days>
<hours>24</hours>
</Locations_2>
<Payments_1>
<amount>1500</amount>
<currency>USD</currency>
</Payments_1>
<Payments_2>
<amount>1800</amount>
<currency>USD</currency>
</Payments_2>
</Member>
</Members>
``
Current code
``
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="xs" version="3.0">
<xsl:output method="xml" indent="yes"/>
<xsl:mode streamable="yes" on-no-match="shallow-copy" use-accumulators="#all"/>
<xsl:accumulator name="loc-count" as="xs:integer" initial-value="0" streamable="yes">
<xsl:accumulator-rule match="Member" select="0"/>
<xsl:accumulator-rule match="Member/Locations" select="$value + 1"/>
</xsl:accumulator>
<xsl:accumulator name="pay-count" as="xs:integer" initial-value="0" streamable="yes">
<xsl:accumulator-rule match="Member" select="0"/>
<xsl:accumulator-rule match="Member/Payments" select="$value + 1"/>
</xsl:accumulator>
<xsl:template match="Locations">
<xsl:element name="Locations_{accumulator-before('loc-count')}">
<xsl:copy-of select="#* | node()"/>
</xsl:element>
</xsl:template>
<xsl:template match="Payments">
<xsl:element name="Payments_{accumulator-before('pay-count')}">
<xsl:copy-of select="#* | node()"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
``
Current Output
<?xml version="1.0" encoding="UTF-8"?>
<Members>
<Member>
<Name>
<fname>Fred</fname>
<id>1234</id>
</Name>
<Locations_1>
<name>Chicago</name>
<days>3</days>
<hours>24</hours>
</Locations_1>
<Locations_2>
<name>Chicago</name>
<days>3</days>
<hours>24</hours>
</Locations_2>
<Payments_1>
<amount>1000</amount>
<currency>USD</currency>
</Payments_1>
<Payments_2>
<amount>1000</amount>
<currency>USD</currency>
</Payments_2>
<Locations_3>
<name>New York</name>
<days>5</days>
<hours>40</hours>
</Locations_3>
<Locations_4>
<name>Boston</name>
<days>4</days>
<hours>32</hours>
</Locations_4>
</Member>
<Member>
<Name>
<fname>Jack</fname>
<id>4567</id>
</Name>
<Locations_1>
<name>New York</name>
<days>5</days>
<hours>30</hours>
</Locations_1>
<Locations_2>
<name>Chicago</name>
<days>3</days>
<hours>24</hours>
</Locations_2>
<Payments_1>
<amount>1500</amount>
<currency>USD</currency>
</Payments_1>
<Payments_2>
<amount>1800</amount>
<currency>USD</currency>
</Payments_2>
</Member>
</Members>
If you want to group the Member child elements by node-name() then I think you need to wrap the xsl:for-each-group into xsl:fork:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:map="http://www.w3.org/2005/xpath-functions/map"
xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="#all" version="3.0">
<xsl:strip-space elements="*"/>
<xsl:output indent="yes"/>
<xsl:mode on-no-match="shallow-copy" streamable="yes" use-accumulators="counters"/>
<xsl:accumulator name="counters" as="map(xs:QName, xs:integer)" initial-value="map{}" streamable="yes">
<xsl:accumulator-rule match="Member" select="map{}"/>
<xsl:accumulator-rule match="Member/*"
select="map:put($value, node-name(), if (map:contains($value, node-name())) then map:get($value, node-name()) + 1 else 1)"/>
</xsl:accumulator>
<xsl:template match="Member">
<xsl:copy>
<xsl:fork>
<xsl:for-each-group select="*" group-by="node-name()">
<xsl:apply-templates select="current-group()"/>
</xsl:for-each-group>
</xsl:fork>
</xsl:copy>
</xsl:template>
<xsl:template match="Member/*">
<xsl:element name="{node-name()}_{accumulator-before('counters')(node-name())}">
<xsl:apply-templates/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
This approach only shows the grouping, it doesn't try to special case Name elements or some other way to not output an index if there is only one such element.
Firstly, my sympathy. XML that uses names like Payments_1 and Payments_2 is really bad news, someone is going to hate you for generating it like this. But if that's the kind of XML you've been told to produce, I guess it's not your job to question it.
As far as the requirements are concerned, you haven't made it clear whether the various kinds of sibling nodes are always grouped as in your example (all Locations, then all Payments, etc), or whether they can be interleaved.
One way you might be able to reduce the volume of code is by having a single accumulator holding a map. The map would use element names as the key and the current sibling count for that element as the value.
<accumulator name="counters" as="map(xs:QName, xs:integer)" initial-value="map{}">
<xsl:accumulator-rule match="Member" select="map{}"/>
<xsl:accumulator-rule match="Member/*" select="map:put($value, node-name(.), if (map:contains($value, node-name(.)) then map:get($value, node-name(.))+1 else 1"/>
</accumulator>
<xsl:template match="Members/*">
<xsl:element name="{name()}_{accumulator-before('counters')(node-name(.))}">
....
Another way to do the conditional map:put is
map:put($value, node-name(.), ($value(node-name(.)), 0)[1] + 1)
I am facing a problem while creating an XSLT for the below transformation. I am relatively new to XSLT transformations. The problem is that i want to count the number of students from distinct countries in the input. I have tried counting based on conditions but it doesn't work as this counting is grouped. Can someone tell me if this can be done is XSLT 1.0.
My Input is
<ns0:DetailsResponse xmlns:ns0="http://MySchema.XSLTSchema">
<Class>1</Class>
<Students>
<StudentName>John</StudentName>
<StudentSurname>Doe</StudentSurname>
<Country>
<CountryName>UK</CountryName>
</Country>
</Students>
<Students>
<StudentName>Cherry</StudentName>
<StudentSurname>Blossom</StudentSurname>
<Country>
<CountryName>US</CountryName>
</Country>
</Students>
<Students>
<StudentName>Ankit</StudentName>
<StudentSurname>Sood</StudentSurname>
<Country>
<CountryName>INDIA</CountryName>
</Country>
</Students>
<Students>
<StudentName>Peter</StudentName>
<StudentSurname>Scott</StudentSurname>
<Country>
<CountryName>UK</CountryName>
</Country>
</Students>
<Students>
<StudentName>Joe</StudentName>
<StudentSurname>Carter</StudentSurname>
<Country>
<CountryName>UK</CountryName>
</Country>
</Students>
<Students>
<StudentName>Anu</StudentName>
<StudentSurname>Mehta</StudentSurname>
<Country>
<CountryName>INDIA</CountryName>
</Country>
</Students>
</ns0:DetailsResponse>
and I want my Output to be like
Output
<ns0:Root xmlns:ns0="http://MySchema.XSLTSchema_Destination">
<DestinationClass>DestinationClass_0</DestinationClass>
<Countries>
<CountryWiseCount>
<Country>INDIA</Country>
<Count>2</Count>
</CountryWiseCount>
<CountryWiseCount>
<Country>UK</Country>
<Count>3</Count>
</CountryWiseCount>
<CountryWiseCount>
<Country>US</Country>
<Count>1</Count>
</CountryWiseCount>
</Countries>
</ns0:Root>
If you are using XSLT 1.0, then Muenchian Grouping will be your friend here.
You are grouping students by country name, so you define a key to look up students like so
<xsl:key name="students" match="Students" use="Country/CountryName"/>
Then, to get the distinct countries, you would match the Students elements that happen to be the elements that occur first in the key for their given country.
<xsl:template
match="Students[generate-id() = generate-id(key('students', Country/CountryName)[1])]">
Then to get the count of all the students for that country, you can just count the key:
<xsl:value-of select="count(key('students', Country/CountryName))"/>
Here is the full XSLT:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:key name="students" match="Students" use="Country/CountryName"/>
<xsl:template match="Students[generate-id() = generate-id(key('students', Country/CountryName)[1])]">
<CountryWiseCount>
<Country>
<xsl:value-of select="Country/CountryName"/>
</Country>
<Count>
<xsl:value-of select="count(key('students', Country/CountryName))"/>
</Count>
</CountryWiseCount>
</xsl:template>
<xsl:template match="Students"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
When applied to your XML, the following is output
<ns0:DetailsResponse xmlns:ns0="http://MySchema.XSLTSchema">
<Class>1</Class>
<CountryWiseCount>
<Country>UK</Country>
<Count>3</Count>
</CountryWiseCount>
<CountryWiseCount>
<Country>US</Country>
<Count>1</Count>
</CountryWiseCount>
<CountryWiseCount>
<Country>INDIA</Country>
<Count>2</Count>
</CountryWiseCount>
</ns0:DetailsResponse>
Note the use of the template <xsl:template match="Students"/> which matches the Students elements which are not first in the key, to stop them being output. The XSLT will always give priority to the more specific template (with the xpath expression), so this template won't ignore everything.
Obviously you would need to extend the XSLT with a template to match class too, but I am sure you can work that out.
I am having trouble adapting an XSLT to handle some extra attribute rules/logic.
I have 'general' and 'specific' XML files that I am merging with this XSLT:
http://www2.informatik.hu-berlin.de/~obecker/XSLT/merge/merge.xslt.html
Source Files
general.xml
<?xml version="1.0" encoding="utf-8"?>
<settings>
<phone-settings e="2">
<language perm="RW">English</language>
<codec1_name idx="1" perm="">0</codec1_name>
<codec1_name idx="2" perm="">0</codec1_name>
</phone-settings>
</settings>
specific.xml
<?xml version="1.0" encoding="utf-8"?>
<settings>
<phone-settings e="2">
<language perm="RW">German</language>
<codec1_name idx="1" perm="R">8</codec1_name>
</phone-settings>
</settings>
XML documents are merged, specific replaces general.
If the attributes & their values match exactly the specific data is used.
To this point the transformation works well, but I have to make adjustments:
Some elements have a 'idx' attribute which identifies that element uniquely
Some elements have a 'perm' attribute which determines if it is read/write by the user.
If the same element exists in both source files, but with a different 'perm' attribute, the XSLT considers it unique and a duplicate element is introduced:
<?xml version="1.0" encoding="utf-8"?>
<settings>
<phone-settings e="2">
<language perm="RW">German</language>
<codec1_name idx="1" perm="">0</codec1_name>
<codec1_name idx="2" perm="">0</codec1_name>
<codec1_name idx="1" perm="R">8</codec1_name>
</phone-settings>
</settings>
Here is the XSLT template that handles the single-node comparision:
<xslt:template name="m:compare-nodes">
<xslt:param name="node1" />
<xslt:param name="node2" />
<xslt:variable name="type1">
<xslt:apply-templates mode="m:detect-type" select="$node1" />
</xslt:variable>
<xslt:variable name="type2">
<xslt:apply-templates mode="m:detect-type" select="$node2" />
</xslt:variable>
<xslt:choose>
<!-- Are $node1 and $node2 element nodes with the same name? -->
<xslt:when test="$type1='element' and $type2='element' and local-name($node1)=local-name($node2) and namespace-uri($node1)=namespace-uri($node2) and name($node1)!=$dontmerge and name($node2)!=$dontmerge">
<!-- Comparing the attributes -->
<xslt:variable name="diff-att">
<!-- same number ... -->
<xslt:if test="count($node1/#*)!=count($node2/#*)">.</xslt:if>
<!-- ... and same name/content -->
<xslt:for-each select="$node1/#*">
<xslt:if test="not($node2/#* [local-name()=local-name(current()) and namespace-uri()=namespace-uri(current()) and .=current()])">.</xslt:if>
</xslt:for-each>
</xslt:variable>
<xslt:choose>
<xslt:when test="string-length($diff-att)!=0">!</xslt:when>
<xslt:otherwise>=</xslt:otherwise>
</xslt:choose>
</xslt:when>
<!-- Other nodes: test for the same type and content -->
<xslt:when test="$type1!='element' and $type1=$type2 and name($node1)=name($node2) and ($node1=$node2 or ($normalize='yes' and normalize-space($node1)= normalize-space($node2)))">=</xslt:when>
<!-- Otherwise: different node types or different name/content -->
<xslt:otherwise>!</xslt:otherwise>
</xslt:choose>
</xslt:template>
I can exclude the 'perm' attribute from the match by doing this:
<!-- ... and same name/content -->
<xslt:for-each select="$node1/#* [name(.)!='perm']">
That solves the duplicate element issue, unfortunately it also means that the value for that attribute in the specific XML file - 'R' in this case is not merged:
<?xml version="1.0" encoding="utf-8"?>
<settings>
<phone-settings e="2">
<language perm="RW">German</language>
<codec1_name idx="1" perm="">8</codec1_name>
<codec1_name idx="2" perm="">0</codec1_name>
</phone-settings>
</settings>
How can I exclude the 'perm' attribute from the uniqueness test while still ensuring its value in the specific XML file is used in the merge?
The desired result from merging the two example files at the top is:
<?xml version="1.0" encoding="utf-8"?>
<settings>
<phone-settings e="2">
<language perm="RW">German</language>
<codec1_name idx="1" perm="R">8</codec1_name>
<codec1_name idx="2" perm="">0</codec1_name>
</phone-settings>
</settings>
Any help with this would be much appreciated!
Thanks.
I can exclude the 'perm' attribute
from the match by doing this:
<!-- ... and same name/content -->
<xslt:for-each select="$node1/#*[name(.)!='perm']">
That solves the duplicate element
issue, unfortunately it also means
that the value for that attribute in
the specific XML file - 'R' in this
case is not merged
How can I exclude the 'perm' attribute
from the uniqueness test while still
ensuring its value in the specific XML
file is used in the merge?
From a brief reading of the quite complex merge-code, it seems that you can achieve your goal by changing this (at line 190):
<xsl:copy-of select="$first1/#*" />
with this:
<xsl:copy-of select="$first2/#*" />
Do note: A better solution than what you are currently doing is to introduce a new global paramere, say $ignore-attributes-in-comparison and specify as value a space-separated string with the names of all attributes that should be ignored in a node comparison.
With other approach (identify elements by name and key attributes), this stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:param name="pSpecific" select="document('specific.xml')"/>
<xsl:key name="kElemByName-Attr"
match="*"
use="concat(name(),'+',#idx,'+',#perm)"/>
<xsl:template match="node()|#*" name="identity">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="*[not(*)]">
<xsl:param name="pSource" select="$pSpecific"/>
<xsl:param name="pCopy" select="true()"/>
<xsl:variable name="vCurrent" select="."/>
<xsl:for-each select="$pSource">
<xsl:variable name="vMatch"
select="key('kElemByName-Attr',
concat(name($vCurrent),'+',
$vCurrent/#idx,'+',
$vCurrent/#perm))"/>
<xsl:for-each select="$vMatch[$pCopy]">
<xsl:call-template name="identity"/>
</xsl:for-each>
<xsl:for-each select="$vCurrent[not($vMatch)]">
<xsl:call-template name="identity"/>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
<xsl:template match="*[*[not(*)]]">
<xsl:variable name="vCurrent" select="."/>
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
<xsl:for-each select="$pSpecific">
<xsl:variable name="vMatch"
select="key('kElemByName-Attr',
concat(name($vCurrent),'+',
$vCurrent/#idx,'+',
$vCurrent/#perm))"/>
<xsl:apply-templates select="$vMatch/node()">
<xsl:with-param name="pSource" select="$vCurrent"/>
<xsl:with-param name="pCopy" select="false()"/>
</xsl:apply-templates>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Output:
<settings>
<phone-settings e="2">
<language perm="RW">German</language>
<codec1_name idx="1" perm="">0</codec1_name>
<codec1_name idx="2" perm="">0</codec1_name>
<codec1_name idx="1" perm="R">8</codec1_name>
</phone-settings>
</settings>