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.
Related
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.
I'm struggling to copy my parent node data when streaming using template match inside an iterate loop with a path from another xml I access via map.
What I'm getting is this:
<?xml version='1.0' encoding='utf-8'?>
<root>
<row>
<Record>1</Record>
<Employee-ID>12345</Employee-ID>
<Authorization-ID>133746</Authorization-ID>
<Date>2021-06-22</Date>
<Quantity>2</Quantity>
<Task-ID>PRJTASK0011134</Task-ID>
<Project-Plan-ID>133746-GP OM Internal Labor-1</Project-Plan-ID>
<Comments/>
<Status>Error: Invalid ID value. '' is not a valid ID value for type =
'Custom_Worktag_13_ID'. Error: Invalid ID value. '' is not a valid ID value for type =
'Custom_Organization_Reference_ID'. Error: Invalid ID value. '133746-GP OM Internal
Labor-1' is not a valid ID value for type = 'Project_Plan_ID'. Error: Invalid ID value.
'12345' is not a valid ID value for type = 'Employee_ID'.</Status>
</row>
</root>
But the output I'm looking for is:
<?xml version='1.0' encoding='utf-8'?>
<root>
<row>
<Record>1</Record>
<Employee-ID>12345</Employee-ID>
<Authorization-ID>133746</Authorization-ID>
<Date>2021-06-22</Date>
<Quantity>2</Quantity>
<Task-ID>PRJTASK0011134</Task-ID>
<Project-Plan-ID>133746-GP OM Internal Labor-1</Project-Plan-ID>
<Comments/>
<Status>Error: Invalid ID value. '' is not a valid ID value for type =
'Custom_Worktag_13_ID'.</Status>
</row>
<row>
<Record>1</Record>
<Employee-ID>12345</Employee-ID>
<Authorization-ID>133746</Authorization-ID>
<Date>2021-06-22</Date>
<Quantity>2</Quantity>
<Task-ID>PRJTASK0011134</Task-ID>
<Project-Plan-ID>133746-GP OM Internal Labor-1</Project-Plan-ID>
<Comments/>
<Status>Error: Invalid ID value. '' is not a valid ID value for type =
'Custom_Organization_Reference_ID'.</Status>
</row>
<row>
<Record>1</Record>
<Employee-ID>12345</Employee-ID>
<Authorization-ID>133746</Authorization-ID>
<Date>2021-06-22</Date>
<Quantity>2</Quantity>
<Task-ID>PRJTASK0011134</Task-ID>
<Project-Plan-ID>133746-GP OM Internal Labor-1</Project-Plan-ID>
<Comments/>
<Status>Error: Invalid ID value. '133746-GP OM Internal Labor-1' is not a valid ID value for
type = 'Project_Plan_ID'.</Status>
</row>
<row>
<Record>1</Record>
<Employee-ID>12345</Employee-ID>
<Authorization-ID>133746</Authorization-ID>
<Date>2021-06-22</Date>
<Quantity>2</Quantity>
<Task-ID>PRJTASK0011134</Task-ID>
<Project-Plan-ID>133746-GP OM Internal Labor-1</Project-Plan-ID>
<Comments/>
<Status>Error: Invalid ID value. '12345' is not a valid ID value for type =
'Employee_ID'.</Status>
</row>
</root>
XSLT Input XML:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<row>
<Record>1</Record>
<Employee-ID>12345</Employee-ID>
<Authorization-ID>133746</Authorization-ID>
<Date>2021-06-22</Date>
<Quantity>2</Quantity>
<Task-ID>PRJTASK0011134</Task-ID>
<Project-Plan-ID>133746-GP OM Internal Labor-1</Project-Plan-ID>
<Comments/>
</row>
</root>
Error Variable Data:
<?xml version="1.0" encoding="UTF-8"?>
<errors>
<error>
<lineNumber>1</lineNumber>
<errorGroup>
<errorRow>
<severity>Error</severity>
<message>Invalid ID value. '' is not a valid ID value for type =
'Custom_Worktag_13_ID'</message>
</errorRow>
<errorRow>
<severity>Error</severity>
<message>Invalid ID value. '' is not a valid ID value for type =
'Custom_Organization_Reference_ID'</message>
</errorRow>
<errorRow>
<severity>Error</severity>
<message>Invalid ID value. '133746-GP OM Internal Labor-1' is not a valid ID value
for type = 'Project_Plan_ID'</message>
</errorRow>
<errorRow>
<severity>Error</severity>
<message>Invalid ID value. '12345' is not a valid ID value for type =
'Employee_ID'</message>
</errorRow>
</errorGroup>
</error>
</errors>
Current XSLT 3 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"
xmlns:map="http://www.w3.org/2005/xpath-functions/map" version="3.0">
<xsl:output method="xml" indent="yes"/>
<xsl:mode name="streaming" streamable="yes" on-no-match="shallow-skip"/>
<xsl:mode name="in-memory" streamable="no"/>
<xsl:variable name="lineKey" as="map(xs:string, element())">
<xsl:map>
<xsl:call-template name="generateErrorFileMap"/>
</xsl:map>
</xsl:variable>
<xsl:template match="root">
<root>
<xsl:apply-templates select="row/copy-of()" mode="in-memory"/>
</root>
</xsl:template>
<xsl:template match="row" mode="in-memory">
<xsl:choose>
<xsl:when test="map:contains($lineKey, Record)">
<xsl:iterate select="map:get($lineKey, Record)/errorGroup/errorRow">
<row>
<!-- Copy Nodes -->
<xsl:apply-templates/>
<Status>
<xsl:value-of select="concat(severity, ': ', message, '.')"/>
</Status>
</row>
</xsl:iterate>
</xsl:when>
<xsl:otherwise>
<row>
<!-- Copy Nodes -->
<xsl:apply-templates/>
<status>
<xsl:value-of select="'Successfully loaded.'"/>
</status>
</row>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- standard copy template -->
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#*"/>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template name="generateErrorFileMap">
<xsl:source-document href="mctx:vars/errorFile" streamable="yes">
<xsl:for-each select="/errors/error/copy-of()">
<xsl:map-entry key="lineNumber => string()">
<map>
<xsl:apply-templates select="errorGroup"/>
</map>
</xsl:map-entry>
</xsl:for-each>
</xsl:source-document>
</xsl:template>
</xsl:stylesheet>
What would be the best approach to achieve my desired output in an optimized way?
I think this is a task for xsl:merge (at least if the rows and errors are sorted by the lineNumber and Record integer value):
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
expand-text="yes">
<xsl:output method="xml" indent="yes"/>
<xsl:mode on-no-match="shallow-copy"/>
<xsl:template name="xsl:initial-template">
<root>
<xsl:merge>
<xsl:merge-source name="record" for-each-source="'record-list.xml'" streamable="yes" select="root/row">
<xsl:merge-key select="xs:integer(Record)"/>
</xsl:merge-source>
<xsl:merge-source name="error" for-each-source="'error-list.xml'" streamable="yes" select="errors/error">
<xsl:merge-key select="xs:integer(lineNumber)"/>
</xsl:merge-source>
<xsl:merge-action>
<xsl:choose>
<xsl:when test="not(current-merge-group('error'))">
<xsl:apply-templates select="current-merge-group('record')">
<xsl:with-param name="status" select="'Successfully loaded.'"/>
</xsl:apply-templates>
</xsl:when>
<xsl:when test="current-merge-group('record') and current-merge-group('error')">
<xsl:for-each select="current-merge-group('error')/errorGroup/errorRow">
<xsl:apply-templates select="current-merge-group('record')">
<xsl:with-param name="status" select="severity || ':' || message"/>
</xsl:apply-templates>
</xsl:for-each>
</xsl:when>
</xsl:choose>
</xsl:merge-action>
</xsl:merge>
</root>
<xsl:comment xmlns:saxon="http://saxon.sf.net/">Run with {system-property('xsl:product-name')} {system-property('xsl:product-version')} {system-property('Q{http://saxon.sf.net/}platform')}</xsl:comment>
</xsl:template>
<xsl:template match="row/*[last()]">
<xsl:param name="status"/>
<xsl:next-match/>
<status>{$status}</status>
</xsl:template>
</xsl:stylesheet>
Run Saxon EE with the -it option instead of the -s source option.
To fix the original code, I think instead of
<xsl:when test="map:contains($lineKey, Record)">
<xsl:iterate select="map:get($lineKey, Record)/errorGroup/errorRow">
<row>
<!-- Copy Nodes -->
<xsl:apply-templates/>
<Status>
<xsl:value-of select="concat(severity, ': ', message, '.')"/>
</Status>
</row>
</xsl:iterate>
</xsl:when>
you want e.g.
<xsl:variable name="record" select="."/>
<xsl:when test="map:contains($lineKey, Record)">
<xsl:iterate select="map:get($lineKey, Record)/errorGroup/errorRow">
<xsl:apply-templates select="$record">
<xsl:with-param name="status" select="severity || ': ' || message || '.'"/>
</xsl:apply-templates>
</xsl:iterate>
</xsl:when>
plus a template (requires expand-text="yes" in scope)
<xsl:template match="row/*[last()]">
<xsl:param name="status"/>
<xsl:next-match/>
<status>{$status}</status>
</xsl:template>
I think that the result of
<xsl:apply-templates select="errorGroup"/>
is going to be an errorGroup element, which means that the result of map:get($lineKey, Record) will be an errorGroup element, which means that map:get($lineKey, Record)/errorGroup will select nothing (since an errorGroup element does not have an errorGroup child). Try changing the type declaration of the map to as="map(xs:string, element(errorGroup))" to strengthen the type checking.
That's the only thing I notice from a quick code check, but you haven't told us how it's failing; it's difficult to diagnose an invisible problem.
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 have an XML as follows:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<ExistingReservations>
<reservations>
<reservation>
<type>1</type>
<start_time>2019-03-09T16:11:14Z</start_time>
<stop_time>2019-03-09T16:23:23Z</stop_time>
</reservation>
<reservation>
<type>2</type>
<start_time>2019-03-09T11:23:12Z</start_time>
<stop_time>2019-03-09T11:32:18Z</stop_time>
</reservation>
<reservation>
<type>2</type>
<start_time>2019-03-09T12:23:12Z</start_time>
<stop_time>2019-03-09T12:32:18Z</stop_time>
</reservation>
</reservations>
</ExistingReservations>
I want to only look at reservations of type '2' and then get the range of dates. i.e. the first start_time and the last end time.
But i'm struggling with my xsl as I can't seem to get the first and last positions.
My xsl is as follows:
<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="ExistingReservations">
<ReservationSchedule>
<xsl:for-each select="//reservation">
<xsl:if test="type='2'">
<period>
<xsl:if test="position()=1">
<start_time><xsl:value-of select="start_time"/><start_time>
</xsl:if>
<xsl:if test="position() = last()">
<end_time><xsl:value-of select="stop_time"/></end_time>
</xsl:if>
</period>
</xsl:if>
</xsl:for-each>
</ReservationSchedule>
</xsl:template>
</xsl:stylesheet>
So I want to transform to the following:
<ReservationSchedule>
<period>
<start_time>2019-03-09T11:23:12Z</start_time>
<end_time>2019-03-09T12:32:18Z</end_time>
</period>
</ReservationSchedule>
I think in the <xsl:if test="position()=1"> is not working because it's looking at the first node whichi s type 1.. i.e. it's igoring the <xsl:if test="type='2'"> logic.
Any help appreciated.
Your xsl:for-each selects all reservations, so position() will be based on that selected node set. The xsl:if will not affect the position() function.
What you need to do is change the select statement itself, so only reservations of type 2 are selected in the first place
<xsl:template match="ExistingReservations">
<ReservationSchedule>
<xsl:for-each select="//reservation[type='2']">
<period>
<xsl:if test="position()=1">
<start_time><xsl:value-of select="start_time"/></start_time>
</xsl:if>
<xsl:if test="position() = last()">
<end_time><xsl:value-of select="stop_time"/></end_time>
</xsl:if>
</period>
</xsl:for-each>
</ReservationSchedule>
</xsl:template>
Alternatively, you can do away with the xsl:for-each and write it like this:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" indent="yes" />
<xsl:template match="ExistingReservations">
<ReservationSchedule>
<xsl:variable name="type2s" select="//reservation[type='2']" />
<period>
<start_time><xsl:value-of select="$type2s[1]/start_time"/></start_time>
<end_time><xsl:value-of select="$type2s[last()]/stop_time"/></end_time>
</period>
</ReservationSchedule>
</xsl:template>
</xsl:stylesheet>
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>