XSLT concatenate input from several nodes in a single output - sorting

I'm trying to work out a transformation that will process an input with several Flights with Departure and Arrival into a single output with the complete route for the flights.
Input is as follows:
<FlightTrip>
<flights>
<departureAirport>
<airportCode>LocB</airportCode>
</departureAirport>
<departureTime>2013-03-28T10:00:00.000</departureTime>
<arrivalAirport>
<airportCode>LocC</airportCode>
</arrivalAirport>
</flights>
<flights>
<departureAirport>
<airportCode>LocA</airportCode>
</departureAirport>
<departureTime>2013-03-27T15:00:00.000</departureTime>
<arrivalAirport>
<airportCode>LocB</airportCode>
</arrivalAirport>
</flights>
<flights>
<departureAirport>
<airportCode>LocC</airportCode>
</departureAirport>
<departureTime>2013-03-30T14:00:00.000</departureTime>
<arrivalAirport>
<airportCode>LocD</airportCode>
</arrivalAirport>
</flights>
</FlightTrip>
The desired output would be this:
<FullTrip>LocA LocB LocC LocD</FullTrip>
I've tried to use foreach inside the output variable but I can't get it right. I also need to sort the input based on the departure date as the Flights can be in a different order (as per the sample input).
Any ideas of how to achieve this?
Thanks a lot!
Bruno

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output indent="yes"/>
<xsl:template match="FlightTrip">
<FullTrip>
<xsl:apply-templates select="flights">
<xsl:sort select="departureTime"/>
</xsl:apply-templates>
</FullTrip>
</xsl:template>
<xsl:template match="flights">
<xsl:value-of select="departureAirport/airportCode"/><xsl:text> </xsl:text>
<xsl:if test="position()=last()">
<xsl:value-of select="arrivalAirport/airportCode"/>
</xsl:if>
</xsl:template>
</xsl:transform>
Will produce:
<FullTrip>LocA LocB LocC LocD</FullTrip>
Working example

Thanks to Joepie for the enlightenment. I had to modify it a bit to get it to work in my environment, ended up using foreach as below:
<xsl:template match="/">
<xsl:variable name="locations">
<xsl:for-each select="/FlightTrip/flights">
<xsl:sort select="departureTime" order="ascending" data-type="text"/>
<xsl:value-of select="concat(departureAirport/airportCode,' - ')"/>
<xsl:if test="position() = last()">
<xsl:value-of select="arrivalAirport/airportCode"/>
</xsl:if>
</xsl:for-each>
</xsl:variable>
<FullTrip>
<xsl:value-of select="$locations"/>
</FullTrip>
</xsl:template>
When applied to the example produces the output below:
<FullTrip>LocA - LocB - LocC - LocD</FullTrip>
Thanks again!

Related

How to get nested nodes from XML to CSV via XSLT

I have XML like below:
<?xml version="1.0" encoding="UTF-8"?>
<Envelope xmlns="http://schemas.microsoft.com/dynamics/2011/01/documents/Message">
<Header>
<MessageId>{70BF3A9B-9111-48D8-93B4-C6232E74307F}</MessageId>
<Action>http://tempuri.org/example/find</Action>
</Header>
<Body>
<MessageParts>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.02" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<pain.001.001.02>
<GrpHdr>
<MsgId>AB01029407</MsgId>
<CreDtTm>2020-05-07T11:23:08</CreDtTm>
<NbOfTxs>2</NbOfTxs>
<CtrlSum>4598</CtrlSum>
<Grpg>MIXD</Grpg>
<InitgPty>
<Nm>MY COMPANY Ltd1</Nm>
<Id>
<OrgId>
<TaxIdNb>GB 823825133</TaxIdNb>
</OrgId>
</Id>
</InitgPty>
</GrpHdr>
<PmtInf>
<PmtInfId>AB01029407</PmtInfId>
<PmtMtd>TRF</PmtMtd>
<PmtTpInf>
<SvcLvl>
<Cd>SEPA</Cd>
</SvcLvl>
</PmtTpInf>
<Dbtr>
<Nm>MY COMPANY Ltd</Nm>
<PstlAdr>
<AdrLine>Address Line 1</AdrLine>
<AdrLine>Address Line 2</AdrLine>
<Ctry>CB</Ctry>
</PstlAdr>
</Dbtr>
<DbtrAcct>
<Id>
<IBAN>98</IBAN>
</Id>
</DbtrAcct>
<DbtrAgt>
<FinInstnId>
<BIC>ABC123</BIC>
</FinInstnId>
</DbtrAgt>
<ChrgBr>SLEV</ChrgBr>
<CdtTrfTxInf>
<PmtId>
<EndToEndId>Not-Provided</EndToEndId>
</PmtId>
<Amt>
<InstdAmt Ccy="CAD">2198.00</InstdAmt>
</Amt>
<CdtrAgt>
<FinInstnId>
<BIC>SWIFT01</BIC>
</FinInstnId>
</CdtrAgt>
<Cdtr>
<Nm>Creditor Name</Nm>
<PstlAdr>
<AdrLine>tests</AdrLine>
<AdrLine>Chicago</AdrLine>
<Ctry>US</Ctry>
</PstlAdr>
</Cdtr>
<CdtrAcct>
<Id>
<IBAN>98</IBAN>
</Id>
</CdtrAcct>
<RmtInf>
<Ustrd>1345</Ustrd>
</RmtInf>
</CdtTrfTxInf>
<CdtTrfTxInf>
<PmtId>
<EndToEndId>Not-Provided</EndToEndId>
</PmtId>
<Amt>
<InstdAmt Ccy="EUR">2400.00</InstdAmt>
</Amt>
<CdtrAgt>
<FinInstnId>
<BIC>SWIFT01</BIC>
</FinInstnId>
</CdtrAgt>
<Cdtr>
<Nm>Creditor Name1</Nm>
<PstlAdr>
<AdrLine>tests</AdrLine>
<AdrLine>Chicago</AdrLine>
<Ctry>US</Ctry>
</PstlAdr>
</Cdtr>
<CdtrAcct>
<Id>
<IBAN>98</IBAN>
</Id>
</CdtrAcct>
<RmtInf>
<Ustrd>123456765</Ustrd>
</RmtInf>
</CdtTrfTxInf>
</PmtInf>
</pain.001.001.02>
</Document>
</MessageParts>
</Body>
</Envelope>
I have XSLT like this:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ns1="http://schemas.microsoft.com/dynamics/2011/01/documents/Message"
xmlns:ns2="urn:iso:std:iso:20022:tech:xsd:pain.001.001.02"
version="1.0">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:apply-templates
select="ns1:Envelope/ns1:Body//ns2:pain.001.001.02//ns2:GrpHdr"/>
</xsl:template>
<xsl:template match="ns2:GrpHdr">
<xsl:value-of select="ns2:CreDtTm"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="ns2:NbOfTxs"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="ns2:CtrlSum"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="ns2:Grpg"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="ns2:InitgPty/Nm"/>
<xsl:text>
</xsl:text> <!-- Line Return -->
</xsl:template>
</xsl:stylesheet>
With this XSLT I am getting only one set..but not able to go beyond one group of elements. Output i got is:
2020-05-07T11:23:08,2,4598,MIXD,
This looks correct only. But i wanted almost all specific nodes. I could not able to get the inner nested elements from a template.
The desired output is:
2020-05-07T11:23:08,2,4598,MIXD,MY COMPANY Ltd1,GB 823825133,AB01029407,TRF,SEPA,MY COMPANY Ltd,Address Line 1,Address Line 2,CB,98,ABC123,SLEV,Not-Provided,2198.00,SWIFT01,Creditor Name,tests,Chicago,US,98,1345
2020-05-07T11:23:08,2,4598,MIXD,MY COMPANY Ltd1,GB 823825133,AB01029407,TRF,SEPA,MY COMPANY Ltd,Address Line 1,Address Line 2,CB,98,ABC123,SLEV,Not-Provided,2400.00,SWIFT01,Creditor Name1,tests,Chicago,US,98,123456765
I am newer to XSLT. Can anyone help with this ?
Thanks in advance.
Try this as your starting point:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ns1="http://schemas.microsoft.com/dynamics/2011/01/documents/Message"
xmlns:ns2="urn:iso:std:iso:20022:tech:xsd:pain.001.001.02">
<xsl:output method="text"/>
<xsl:template match="/ns1:Envelope">
<!-- data from header -->
<xsl:variable name="header" select="ns1:Body/ns1:MessageParts/ns2:Document/ns2:pain.001.001.02/ns2:GrpHdr" />
<xsl:value-of select="$header/ns2:CreDtTm"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="$header/ns2:NbOfTxs"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="$header/ns2:CtrlSum"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="$header/ns2:Grpg"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="$header/ns2:InitgPty/ns2:Nm"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="$header/ns2:InitgPty/ns2:Id/ns2:OrgId/ns2:TaxIdNb"/>
<xsl:text>,</xsl:text>
<!-- data from pmt -->
<xsl:variable name="pmt" select="ns1:Body/ns1:MessageParts/ns2:Document/ns2:pain.001.001.02/ns2:PmtInf" />
<xsl:value-of select="$pmt/ns2:PmtMtd"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="$pmt/ns2:Dbtr/ns2:Nm"/>
<xsl:text>,</xsl:text>
<!-- CONTINUE HERE -->
</xsl:template>
</xsl:stylesheet>
Note that this assumes there is only one record in the input XML and therefore only one row in the output CSV. Your XML is structured in a way that allows multiple nodes of the same kind at various level of the hierarchy. If you want to reflect this in your CSV, you need to decide which node will represent a record and adjust the stylesheet so that it creates a separate row for each instance of such node - see an example here: https://stackoverflow.com/a/55311500/3016153

Constructing variable with template in XSLT and then applying xpath

I am using following xslt
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.test.com/services/test/test/v1">
<xsl:output method="xml" encoding="UTF-8"
omit-xml-declaration="yes" indent="yes" />
<xsl:strip-space elements="*" />
<xsl:template match="/">
<xsl:variable name="mytree">
<xsl:call-template name="myvariable">
</xsl:call-template>
</xsl:variable>
<xsl:choose>
<xsl:when test="count($mytree/foos/foo) > 1">
<xsl:copy-of select="$mytree"/>
</xsl:when>
<xsl:otherwise>
<error>test</error>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="myvariable">
<foos>
<foo>bar1</foo>
<foo>bar2</foo>
<foo>bar3</foo>
<foo>bar4</foo>
</foos>
</xsl:template>
</xsl:stylesheet>
When i use above xslt it should be following output
<foos xmlns="http://www.test.com/services/test/test/v1">
<foo>bar1</foo>
<foo>bar2</foo>
<foo>bar3</foo>
<foo>bar4</foo>
</foos>
but it is
<error xmlns="http://www.test.com/services/test/test/v1">test</error>
when i remove the xmlns="http://www.test.com/services/test/test/v1" output is proper. Not sure what is happening?
Well, with any XML, whether constructed inside of your XSLT or read from a source, if you have elements in a certain namespace, then, to select them with XPath in XSLT, in XSLT 2 you have two options, either use xpath-default-namespace="http://www.test.com/services/test/test/v1" (e.g. <xsl:when test="count($mytree/foos/foo) > 1" xpath-default-namespace="http://www.test.com/services/test/test/v1">) or bind the namespace to a prefix (e.g. <xsl:when xmlns:v1="http://www.test.com/services/test/test/v1" test="count($mytree/v1:foos/v1:foo) > 1">).
You can use these approaches on an ancestor element, for instance the root element of the stylesheet, if it does not interfere with other selections you want to make.
You have to specify qualified element names in your XPath expression to address the foos and foo elements in your default namespace http://www.test.com/services/test/test/v1:
Register the default namespace once more with a namespace prefix (e.g. myns): xmlns:myns="http://www.test.com/services/test/test/v1"
Use that namepace prefix in your XPath expressions to address nodes in that namespace (e.g. myns:foos/myns:foo).
Add exclude-result-prefixes="myns" to suppress the myns prefix in your result document.
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.test.com/services/test/test/v1"
xmlns:myns="http://www.test.com/services/test/test/v1"
exclude-result-prefixes="myns">
…
<xsl:template match="/">
…
<xsl:choose>
<xsl:when test="count($mytree/myns:foos/myns:foo) > 1">
<xsl:copy-of select="$mytree"/>
</xsl:when>
<xsl:otherwise>
<error>test</error>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
…
</xsl:stylesheet>
see XSLT Fiddle
If you only had an XSLT 1.0 processor at hand, you would need the EXSLT node-set function to access the $mytree variables from the result tree:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.test.com/services/test/test/v1"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl"
xmlns:myns="http://www.test.com/services/test/test/v1"
exclude-result-prefixes="myns">
…
<xsl:template match="/">
…
<xsl:choose>
<xsl:when test="count(exsl:node-set($mytree)/myns:foos/myns:foo) > 1">
<xsl:copy-of select="$mytree"/>
</xsl:when>
<xsl:otherwise>
<error>test</error>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
…
</xsl:stylesheet>
Use code for remove namespace
<xsl:template match="#*[namespace-uri() = 'http://www.test.com/services/test/test/v1']"/>

How to eliminate all <TAG/> and all attribute="" by XSLT?

In a xsl:stylesheet I have this "identity like" transform, to eliminate comments, empty (terminal) tags and empty attributes... But the second xsl:when not works
<xsl:template match="node()">
<xsl:choose>
<xsl:when test="name()='p' and not(./*) and not(normalize-space(.))"></xsl:when>
<xsl:when test="not(name()='img') and not(name()='br') and not(./*) and not(text())"
></xsl:when> <!-- this line NOT WORKS -->
<xsl:otherwise><xsl:copy><xsl:apply-templates select="#*|node()"/></xsl:copy></xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="#*">
<xsl:choose>
<xsl:when test="not(normalize-space(.))"></xsl:when>
<xsl:otherwise><xsl:copy><xsl:apply-templates select="#*|node()"/></xsl:copy></xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="comment()"></xsl:template>
Whow to express condition to empty tags in this context?
PS: the "empty rules" are explained here, I try to use it, but not see why not working.
An empty element is an element with no child nodes.
Template match priority is your friend ... the following should be the kind of identity stylesheet that meets your description plus what I think you are doing with image and break elements.
<?xml version="1.0" encoding="US-ASCII"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<!--toss these-->
<xsl:template match="comment() |
*[not(node())] |
#*[not(normalize-space())]"/>
<!--preserve these-->
<xsl:template match="img|br" priority="1">
<xsl:call-template name="identity"/>
</xsl:template>
<!--preserve everything else-->
<xsl:template match="#*|node()" name="identity">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>

Longer node in XPath

I'd like to use XPath to retrieve the longer of two nodes.
E.g., if my XML is
<record>
<url1>http://www.google.com</url1>
<url2>http://www.bing.com</url2>
</record>
And I do document.SelectSingleNode(your XPath here)
I would expect to get back the url1 node. If url2 is longer, or there is no url1 node, I'd expect to get back the url2 node.
Seems simple but I'm having trouble figuring it out. Any ideas?
This works for me, but it is ugly. Cannot you do the comparison outside XPath?
record/*[starts-with(name(),'url')
and string-length(.) > string-length(preceding-sibling::*[1])
and string-length(.) > string-length(following-sibling::*[1])]/text()
<xsl:for-each select="*">
<xsl:sort select="string-length(.)" data-type="number"/>
<xsl:if test="position() = last()">
<xsl:copy-of select="."/>
</xsl:if>
</xsl:for-each>
Even works in XSLT 1.0!
Use this single XPath expression:
/*/*[not(string-length(preceding-sibling::*|following-sibling::*)
>
string-length()
)
]
XSLT - based verification:
<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:template match="/">
<xsl:copy-of select=
"/*/*[not(string-length(preceding-sibling::*|following-sibling::*)
>
string-length()
)
]"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<record>
<url1>http://www.google.com</url1>
<url2>http://www.bing.com</url2>
</record>
the Xpath expression is evaluated and the result of this evaluation (the selected element) is copied to the output:
<url1>http://www.google.com</url1>

Modify an XML node using [MS] XSLT Script

I would like to select a node and modify its attributes and child-nodes using an
xsl:script function. In addition, templates matching child-nodes of that node should
STILL perform their job (after script is done processing the node).
Can it be done using XSLT?
Can you please provide an example / skeleton for such a transformation?
Yes, it can be done. I don't seem to see what the problem is because the XML (or whatever output) of an XSL script is buffered independently from its input.
This is illustrated in the following example whereby a simple XSL script copies an input XML document mostly as-is, changing a few things:
the root element name and attribute
flattening by removing the element from the hierarchy
dropping the results/date element
rename the item's 'source' attribute 'origin'
change the item's 'level' attribute value
rename the FirstName and LastName elements of the item elements
Sample input
<?xml version="1.0" encoding="ISO-8859-1"?>
<MyRoot version="1.2">
<results>
<info>Alpha Bravo</info>
<author>Employee No 321</author>
<date/>
<item source="www" level="6" cost="33">
<FirstName>Jack</FirstName>
<LastName>Frost</LastName>
<Date>1998-10-30</Date>
<Organization>Lemon growers association</Organization>
</item>
<item source="db-11" level="1" cost="65" qry="routine 21">
<FirstName>Mike</FirstName>
<LastName>Black</LastName>
<Date>2006-10-30</Date>
<Organization>Ford Motor Company</Organization>
</item>
</results>
</MyRoot>
Output produced
<?xml version="1.0" encoding="utf-16"?>
<MyNewRoot version="0.1">
<author>Employee No 321</author>
<info>Alpha Bravo</info>
<item cost="33" origin="www" level="77">
<GivenName>Jack</GivenName>
<FamilyName>Frost</FamilyName>
<Date>1998-10-30</Date>
<Organization>Lemon growers association</Organization>
</item>
<item cost="65" qry="routine 21" origin="db-11" level="77">
<GivenName>Mike</GivenName>
<FamilyName>Black</FamilyName>
<Date>2006-10-30</Date>
<Organization>Ford Motor Company</Organization>
</item>
</MyNewRoot>
XSL script
<?xml version='1.0'?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
exclude-result-prefixes="#default">
<xsl:template match="MyRoot">
<xsl:call-template name="MainTemplate">
</xsl:call-template>
</xsl:template>
<xsl:template name="MainTemplate">
<MyNewRoot version="0.1">
<xsl:copy-of select="results/author" />
<xsl:copy-of select="results/info" />
<xsl:for-each select="results/item">
<xsl:call-template name="FixItemElement"/>
</xsl:for-each>
</MyNewRoot>
</xsl:template>
<xsl:template name="FixItemElement">
<xsl:copy>
<xsl:copy-of select="#*[not(name()='source' or name()='level')]" />
<xsl:attribute name="origin">
<xsl:value-of select="#source"/>
</xsl:attribute>
<xsl:attribute name="level">
<xsl:value-of select="77"/>
</xsl:attribute>
<xsl:for-each select="descendant::*">
<xsl:choose>
<xsl:when test="local-name(.) = 'FirstName'">
<GivenName>
<xsl:value-of select="."/>
</GivenName>
</xsl:when>
<xsl:when test="local-name(.) = 'LastName'">
<FamilyName>
<xsl:value-of select="."/>
</FamilyName>
</xsl:when>
<xsl:otherwise>
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:copy>
</xsl:template>

Resources