How to traverse and sum XML document with many-to-many relationship using XSLT 1.0? - xslt-1.0

I need help to process following XML document using XSL 1.0, and I don't have the words to describe what the actual problem is.
I have a collection of items collectionA I'd like to process. collectionA is related to collectionB from which I'd like to get the sum of all related num values. The relationship is a many-to-many relationship, defined by the collectionC items, that connects the A and B items.
In any other object model based programming language I would use a loop. Problem is that I don't see how to both loop AND to collect the sum of all values in xslt 1.0...?
<root>
<collectionA>
<id>1</id>
</collectionA>
<collectionA>
<id>2</id>
</collectionA>
<collectionB>
<id>1</id>
<num>11</num>
</collectionB>
<collectionB>
<id>2</id>
<num>22</num>
</collectionB>
<collectionB>
<id>3</id>
<num>33</num>
</collectionB>
<collectionB>
<id>4</id>
<num>44</num>
</collectionB>
<collectionC>
<collectionAid>1</collectionAid>
<collectionBid>1</collectionBid>
<collectionBid>2</collectionBid>
</collectionC>
<collectionC>
<collectionAid>2</collectionAid>
<collectionBid>3</collectionBid>
<collectionBid>4</collectionBid>
</collectionC>
</root>
Expected result is:
<root>
<collectionA>
<id>1</id>
<sum>33</sum>
</collectionA>
<collectionA>
<id>2</id>
<sum>77</sum>
</collectionA>
</root>
Anyone!?

I don't see how to both loop AND to collect the sum of all values
You do not need to do either one of these. All you need is a pair of keys to resolve the cross-references. Then use them to select the related nodes and sum their values:
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="b" match="collectionB" use="id" />
<xsl:key name="c" match="collectionC" use="collectionAid" />
<xsl:template match="/root">
<xsl:copy>
<xsl:for-each select="collectionA">
<xsl:copy>
<xsl:copy-of select="id"/>
<sum>
<xsl:value-of select="sum(key('b', key('c', id)/collectionBid)/num)"/>
</sum>
</xsl:copy>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>

Related

XSLT to put one particular XML element before all others

XSLT 1.0 solution required. My question is similar to XSLT Change element order and I'll take this answer if I have to, but I hope I can do something like 'put this_element first, and retain the original order of all the rest of them'. The input is something like this, where ... can be any set of simple elements or text nodes, but no processing instructions nor comments. See below also.
<someXML>
<recordList>
<record priref="1" created="2009-06-04T16:54:35" modification="2014-12-16T14:56:51" selected="False">
...
<collection_type>3D</collection_type>
...
<object_category>headgear</object_category>
<object_name>hat</object_name>
<object_number>060998</object_number>
...
</record>
<record priref="3" created="2009-06-04T11:54:35" modification="2020-08-05T18:24:33" selected="False">
...
<collection_type>3D</collection_type>
<description>a very elaborate coat</description>
<object_category>clothing</object_category>
<object_name>coat</object_name>
<object_number>060998</object_number>
</record>
</recordList>
</someXML>
This would be the desired output.
<someXML>
<recordList>
<record priref="1" created="2009-06-04T16:54:35" modification="2014-12-16T14:56:51" selected="False">
<object_category>clothing</object_category>
...
<collection_type>3D</collection_type>
...
<object_name>hat</object_name>
<object_number>060998</object_number>
...
</record>
<record priref="3" created="2009-06-04T11:54:35" modification="2020-08-05T18:24:33" selected="False">
<object_category>clothing</object_category>
...
<collection_type>3D</collection_type>
<description>a very elaborate coat</description>
<object_name>coat</object_name>
<object_number>060998</object_number>
</record>
</recordList>
</someXML>
It's probably OK if object_category is put first, and then occurs again later on in the record, i.e. in the tags in their original order.
I'll add some background. There's this API producing about 900.000 XML records with different tags (element names) in alphabetical order, per record. There are about 170 different element names (that's why I don't want to have to list them all individually, unless there's no other way). The XML is ingested into this graph database. That takes time, but it could be sped up if we see the object_category as the first element in the record.
Edit: We can configure the API, but not the C# code behind the API. We step through the database, step by step ingesting chunks of ~100 records. If we specify nothing else, we get the XML as exemplified above. We can also specify an XSL sheet to transform the XML. That's what we want to do here.
The example is ambiguous, because we don't know what all those ... placeholders stand for. I suppose this should work for you:
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:strip-space elements="*"/>
<!-- identity transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="record">
<xsl:copy>
<xsl:apply-templates select="#*"/>
<xsl:apply-templates select="object_category"/>
<xsl:apply-templates select="node()[not(self::object_category)]"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>

XSLT for-each, trying to create header-line structure

This is not the browser type XSLT, this is for processing data (SAP B1 Integration Framework). Suppose we have two SQL Tables, HEADER and LINE and we want to avoid the kind of work where we first SELECT from the HEADER and then launch a separate select for the lines for each, because that requires "visual programming", and we like writing code more than connecting arrows. So we are sending the server a query like SELECT * FROM HEADER, SELECT * FROM LINES and we get an XML roughly like this:
<ResultSets>
<ResultSet>
<Row><MHeaderNum>1</MHeaderNum></Row>
<Row><MHeaderNum>2</MHeaderNum></Row>
<Row><MHeaderNum>3</MHeaderNum></Row>
</ResultSet>
<ResultSet>
<Row><LineNum>1</LineNum> <HeaderNum>1</HeaderNum></Row>
<Row><LineNum>2</LineNum> <HeaderNum>2</HeaderNum></Row>
<Row><LineNum>1</LineNum> <HeaderNum>3</HeaderNum></Row>
<Row><LineNum>2</LineNum> <HeaderNum>1</HeaderNum></Row>
<Row><LineNum>1</LineNum> <HeaderNum>2</HeaderNum></Row>
<Row><LineNum>2</LineNum> <HeaderNum>3</HeaderNum></Row>
</ResultSet>
so we think we are imperative, procedural programmers and pull a
<xsl:for-each select="//ResultSets/Resultset[1]/Row">
do stuff with header data
<xsl:for-each select="//ResultSets/Resultset[2]/Row[HeaderNum=MHeaderNum]">
do stiff with the line data beloning to this particular header
</xsl:for-each>
</xsl:for-each>
And of course this does not blinkin' work because MHeaderNum went out of context like grunge went out of fashion, and we cannot save it into a variable either because we will not be update that variable, as XSLT is something sort of an immutable functional programming language.
But fear not, says an inner voice, because XSLT gurus can solve things like that with templates. Templates, if I understand it, are sort of XSLT's take on functions. They can be recursive and stuff like that. So can they be used to solve problems like this?
And of course we are talking about XSLT 1.0 because I don't know whether Java ever bothered to implement the later versions, but SAP certainly did not bother to used said, hypothetical implementation.
Or should I really forget about this and just connect my visual arrows? The thing is, SQL is not supposed to be used in such an iterate through headers then iterate through lines ways. What I am trying to do is what makes an SQL database a happy, get a big ol' chunk of data out of it and then process it somewhere else, not bother it with seventy zillion tiny queries. And in our case the somewhere else is sadly XSLT, although technically I could try JavaScript as well as SAP added a Nashorn to this pile of mess as well, but maybe it is solvable in "pure" XSL?
Whether XSLT 1 or later and whether with templates and for-each, the current() function exists: //ResultSets/Resultset[2]/Row[HeaderNum=current()/MHeaderNum].
The best way to resolve cross-references is by using a key.
For example, the following stylesheet:
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="line-by-header" match="ResultSet[2]/Row" use="HeaderNum" />
<xsl:template match="/ResultSets">
<output>
<xsl:for-each select="ResultSet[1]/Row">
<header num="{MHeaderNum}">
<xsl:for-each select="key('line-by-header', MHeaderNum)">
<line>
<xsl:value-of select="LineNum"/>
</line>
</xsl:for-each>
</header>
</xsl:for-each>
</output>
</xsl:template>
</xsl:stylesheet>
when applied to the following input:
XML
<ResultSets>
<ResultSet>
<Row><MHeaderNum>1</MHeaderNum></Row>
<Row><MHeaderNum>2</MHeaderNum></Row>
<Row><MHeaderNum>3</MHeaderNum></Row>
</ResultSet>
<ResultSet>
<Row><LineNum>1</LineNum> <HeaderNum>1</HeaderNum></Row>
<Row><LineNum>2</LineNum> <HeaderNum>2</HeaderNum></Row>
<Row><LineNum>3</LineNum> <HeaderNum>3</HeaderNum></Row>
<Row><LineNum>4</LineNum> <HeaderNum>1</HeaderNum></Row>
<Row><LineNum>5</LineNum> <HeaderNum>2</HeaderNum></Row>
<Row><LineNum>6</LineNum> <HeaderNum>3</HeaderNum></Row>
</ResultSet>
</ResultSets>
will return:
Result
<?xml version="1.0" encoding="UTF-8"?>
<output>
<header num="1">
<line>1</line>
<line>4</line>
</header>
<header num="2">
<line>2</line>
<line>5</line>
</header>
<header num="3">
<line>3</line>
<line>6</line>
</header>
</output>
You can try the following XSLT. It is using three XSLT templates.
Because desired output is unknown, I placed some arbitrary processing for each of the header and line item templates.
XSLT
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<root>
<xsl:apply-templates/>
</root>
</xsl:template>
<xsl:template match="/ResultSets/ResultSet[1]">
<ResultSet1>
<xsl:for-each select="Row">
<r>
<xsl:value-of select="MHeaderNum"/>
</r>
</xsl:for-each>
</ResultSet1>
</xsl:template>
<xsl:template match="/ResultSets/ResultSet[2]">
<ResultSet2>
<xsl:for-each select="Row">
<xsl:copy-of select="."/>
</xsl:for-each>
</ResultSet2>
</xsl:template>
</xsl:stylesheet>

How to get child node value which has namespace on it?

I am completly new to XSLT and my goal is to retrieve value from a node which has namespace on it. My XML looks as below.
<WBIFNMsg>
<AppData>
<AppName>INFM</AppName>
<MsgType>Notification</MsgType>
<MsgStatus>Success</MsgStatus>
</AppData>
<AppMsg>
<Document xmlns="urn:swift:xsd:setr.010.001.03">
<SbcptOrdrV03>
<MsgId>
<Id>29331095XXXXML</Id>
<CreDtTm>2019-06-07T10:30:43.681+02:00</CreDtTm>
</MsgId>
</SbcptOrdrV03>
</Document>
</AppMsg>
</WBIFNMsg>
I am trying to get the value of Id tag which is under document/SbcptOrdrV03/MsgId/ via the xslt. As the document tag has the namespace I have included it in my xslt but still I couldn't retrieve the value.
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:b="urn:swift:xsd:setr.010.001.03" exclude-result-prefixes="b">
<xsl:output method="xml" omit-xml-declaration="yes"/>
<xsl:template match="/">
<data>
<messageDataApps>
<field>
<fieldName>SENDER.STP</fieldName>
<value>
<xsl:value-of select="b:WBIFNMsg/b:AppMsg/b:Document/b:SbcptOrdrV03/b:MsgId/b:id"/>
</value>
</field>
</messageDataApps>
</data>
</xsl:template>
</xsl:stylesheet>
Appreciate if anyone could help on this. Thanks in advance.
Only Document and its descendants are in a namespace and require using a prefix when addressing them. Instead of:
<xsl:value-of select="b:WBIFNMsg/b:AppMsg/b:Document/b:SbcptOrdrV03/b:MsgId/b:id"/>
try:
<xsl:value-of select="WBIFNMsg/AppMsg/b:Document/b:SbcptOrdrV03/b:MsgId/b:Id"/>
Note also that XML is case-sensitive: b:id is not the same thing as b:Id.

how to transform a parameter using value-of and select

I am using xslt to transform an xml document into another xml document.
in the below code it's only getting the value of the email address for the first review and using that for all the reviews, instead of getting the email address for each review. I know it's not good to use // but when I just use Review/UserEmailAddress the value is blank and I don't know how else to do it.
Here's my input xml:
<Product id="867776000050">
<ExternalId>867776000050</ExternalId>
<Reviews>
<Review id="3924" removed="false">
<UserProfileReference id="Haliley">
<ExternalId>Haliley</ExternalId>
<DisplayName>Haliley</DisplayName>
</UserProfileReference>
<UserEmailAddress>hbonb#yahoo.com</UserEmailAddress>
</Review>
<Review id="3919" removed="false">
<UserProfileReference id="PaulineTincher">
<ExternalId>PaulineTincher</ExternalId>
<DisplayName>PaulineTincher</DisplayName>
</UserProfileReference>
<UserEmailAddress>pt59#msn.com</UserEmailAddress>
</Review>
</Reviews>
</Product>
Here's my stylesheet:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" omit-xml-declaration="no" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Review/UserProfileReference">
<xsl:variable name="userid"><xsl:value-of select="ExternalId"/>
<xsl:value-of select="substring-before(//Review/UserEmailAddress, '#')"/></xsl:variable>
<UserProfileReference id="{$userid}">
<ExternalId><xsl:value-of select="ExternalId"/><xsl:value-of select="substring-before(//Review/UserEmailAddress, '#')"/></ExternalId>
<DisplayName><xsl:value-of select="DisplayName"/></DisplayName>
</UserProfileReference>
</xsl:template>
</xsl:stylesheet>
I am trying to make the UserProfileReference unique by appending the first part of the email address to the existing value.
In my results below the UserProfileReference id value for the first review is correct, it appends the value of the UserEmailAddress to the id.
But for review 2, it uses the email address from review 1, not review 2. I've spent a ton of time on this and just can't figure it out. Please help!
<?xml version="1.0" encoding="UTF-8"?>
<Product id="867776000050">
<ExternalId>867776000050</ExternalId>
<Reviews>
<Review id="3924" removed="false">
<UserProfileReference id="Halileyhbonb">
<ExternalId>Halileyhbonb</ExternalId>
<DisplayName>Haliley</DisplayName>
</UserProfileReference>
<UserEmailAddress>hbonb#yahoo.com</UserEmailAddress>
</Review>
<Review id="3919" removed="false">
<UserProfileReference id="PaulineTincherhbonb">
<ExternalId>PaulineTincherhbonb</ExternalId>
<DisplayName>PaulineTincher</DisplayName>
</UserProfileReference>
<UserEmailAddress>pt59#msn.com</UserEmailAddress>
</Review>
</Reviews>
</Product>
You are using a wrong expression: // acts globally, hence //Review/UserEmailAddress returns a nodeset with all Review/UserEmailAddresses which can globally be found and uses its first item in the substring-before(...). So in both case it's the same item.
A solution is to use a relative path (relative to the context node) like ../UserEmailAddress. So replace both occurrences of
substring-before(//Review/UserEmailAddress, '#')
with
substring-before(../UserEmailAddress, '#')
and it will work as desired.

SQL query for Dynamic nodes in XML [duplicate]

This question already has an answer here:
SQL Data as XML Element
(1 answer)
Closed 5 years ago.
Our table is like
StudentNo Name Subject Mark Grade
1 John English 41 A
1 John Hindi 42 B
We want an XML format from this table as follows.
<Student>
<Name>John</Name>
<Subject>
<English>
<Mark>41</Mark>
<Grade>A</Grade>
</English>
<Hindi>
<Mark>42</Mark>
<Grade>B</Grade>
</Hindi>
</Subject>
<Student>
Here the subject name nodes should be generated dynamically.
This is very similar to SQL Data as XML Element - so much so that I think it might be a duplicate - but I want to explain a bit more for your context why this isn't the best idea. In my answer to that question, I show a really hacky way that you could do this, but it's not the best idea.
Your XML will be nearly impossible to create a schema for. Any consumer of that XML will never be able to be sure what values might appear as elements. Rather than try to create dynamic elements, you should probably use attributes of some sort. You could even use xsi:type to create an abstract type in your XML of sorts (although in my example I'm just using a plain old attribute - you could pick whatever attribute will make the most sense for your consumers). The Query for that XML would be:
declare #subjects TABLE(studentno int, name varchar(10), subjecT varchar(10), mark int, grade char(1))
INSERT #subjects
VALUES
(1, 'John','English', 41,'A'),
(1, 'John','Hindi', 42,'B')
select
s.Name
,(SELECT
s2.Subject as '#type'
,s2.Mark
,s2.Grade
FROM #subjects s2
WHERE s2.studentno = s.studentno
FOR XML PATH('Subject'), ROOT('Subjects'), TYPE)
from #subjects s
GROUP BY s.name, s.studentno
FOR XML PATH('Student')
produces:
<Student>
<Name>John</Name>
<Subjects>
<Subject type="English">
<Mark>41</Mark>
<Grade>A</Grade>
</Subject>
<Subject type="Hindi">
<Mark>42</Mark>
<Grade>B</Grade>
</Subject>
</Subjects>
</Student>
This XML will be possible to make sense of by consumers, where they can, for example, iterate the subjects without knowing what subjects might be there (and without needing to resort to assuming that every direct child of Subjects is in fact a subject and not some other type of node that got added in a new version of the schema).
If you really need that output, I'd prefer to use XSLT to transform the output above to your format, e.g.:
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" encoding="UTF-8" indent="yes" />
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates />
</xsl:copy>
</xsl:template>
<xsl:template match="Subject">
<xsl:element name="{#type}">
<xsl:apply-templates />
</xsl:element>
</xsl:template>
<xsl:template match="Subjects">
<xsl:element name="Subject">
<xsl:apply-templates />
</xsl:element>
</xsl:template>
</xsl:transform>
gets you
<?xml version="1.0" encoding="UTF-8"?>
<Student>
<Name>John</Name>
<Subject>
<English>
<Mark>41</Mark>
<Grade>A</Grade>
</English>
<Hindi>
<Mark>42</Mark>
<Grade>B</Grade>
</Hindi>
</Subject>
</Student>
Note you can't do this completely with SQL Server though - you'd have to resort to building the XML string and casting it as XML, as in my other answer.