Deduplicate and merge memory XML - xslt

I have /credit-entity.xml (target)
<credit>
<entity>
<quality>Investment Grade</quality>
<outlook>Positive</outlook>
<LT-CRR>Aa3</LT-CRR>
<UUID>207</UUID>
<issuer>AA Bank NV</issuer>
<segment>Financial Institutions</segment>
<sector>Bank</sector>
</entity>
<entity>
<quality>Investment Grade</quality>
<outlook>Stable</outlook>
<LT-CRR>Aa3</LT-CRR>
<UUID>203</UUID>
<issuer>GS Bank Europe SE</issuer>
<segment>Financial Institutions</segment>
<sector>Securities & Exchanges</sector>
<date>2022-02-17</date>
<rating>A2</rating>
</entity>
<entity>
<quality>Investment Grade</quality>
<outlook>Stable</outlook>
<ST>(P)P-2</ST>
<UUID>118</UUID>
<issuer>RMGS Services GmbH</issuer>
<segment>Corporates</segment>
<sector>Pharmaceuticals</sector>
<date>2021-12-22</date>
<rating>A2</rating>
</entity>
</credit>
/credit-rating.xml (source)
<ratings>
<rating>
<UUID>207</UUID>
<issuer>AA Bank NV</issuer>
<date>2022-02-07</date>
<rating>Aa3</rating>
</rating>
<rating>
<UUID>203</UUID>
<issuer>GS Bank Europe SE</issuer>
<date>2022-01-31</date>
<rating>A1</rating>
</rating>
<rating>
<LT>A2</LT>
<LT-Type>LT Counterparty Risk Rating - Fgn Curr</LT-Type>
<UUID>233</UUID>
<issuer>AHR AG - Public sector Pfandbriefe</issuer>
<segment>Financial Institutions</segment>
<sector>Covered Bonds</sector>
<date>2022-01-25</date>
<rating>A3</rating>
</rating>
<rating>
<UUID>118</UUID>
<issuer>RMGS Services GmbH</issuer>
<date>2022-02-24</date>
<rating>A1</rating>
</rating>
</ratings>
My xsl is to:
-) match /ratings/rating/UUID with /credit/entity/UUID. If there is matched UUID, compare and get the latest date, create a new element latest with the UUID’s latest date and rating.
-) If no matched UUID in /credit-rating.xml, move its date and rating to form the new element latest.
-) If UUID is in source /credit-rating.xml but missing in target /credit-entity.xml, e.g. UUID 233. Then copy /ratings/rating to target and create new element latest with its date and rating.
Expected output:
<credit>
<entity>
<quality>Investment Grade</quality>
<outlook>Positive</outlook>
<LT-CRR>Aa3</LT-CRR>
<UUID>207</UUID>
<issuer>AA Bank NV</issuer>
<segment>Financial Institutions</segment>
<sector>Bank</sector>
<latest>
<date>2022-02-07</date>
<rating>Aa3</rating>
</latest>
</entity>
<entity>
<quality>Investment Grade</quality>
<outlook>Stable</outlook>
<LT-CRR>Aa3</LT-CRR>
<UUID>203</UUID>
<issuer>GS Bank Europe SE</issuer>
<segment>Financial Institutions</segment>
<sector>Securities & Exchanges</sector>
<latest>
<date>2022-02-17</date>
<rating>A2</rating>
</latest>
</entity>
<entity>
<quality>Investment Grade</quality>
<outlook>Stable</outlook>
<ST>(P)P-2</ST>
<UUID>118</UUID>
<issuer>RMGS Services GmbH</issuer>
<segment>Corporates</segment>
<sector>Pharmaceuticals</sector>
<latest>
<date>2022-02-24</date>
<rating>A1</rating>
</latest>
</entity>
<entity>
<LT>A2</LT>
<LT-Type>LT Counterparty Risk Rating - Fgn Curr</LT-Type>
<UUID>233</UUID>
<issuer>AHR AG - Public sector Pfandbriefe</issuer>
<segment>Financial Institutions</segment>
<sector>Covered Bonds</sector>
<latest>
<date>2022-01-25</date>
<rating>A3</rating>
</latest>
</entity>
</credit>
But somehow my code didn’t work. Can anyone help to fix the issue?
<xsl:variable name="ratingEntity" select="doc('/credit-rating.xml')/ratings/rating"/>
<xsl:variable name="ID">
<xsl:sequence select="credit/entity/UUID"/>
</xsl:variable>
<xsl:template match="credit">
<xsl:copy>
<xsl:for-each select="entity">
<xsl:variable name="uuid" select="UUID"/>
<xsl:copy>
<xsl:choose>
<xsl:when test="not(exists(rating)) or date le $ratingEntity[UUID eq $uuid]/date" >
<xsl:copy-of select="./* except (date, rating)"/>
<latest>
<xsl:call-template name="latest">
<xsl:with-param name="ratingEntity" select="$ratingEntity[UUID eq $uuid]" />
</xsl:call-template>
</latest>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="./* except (date, rating)"/>
<latest>
<xsl:copy-of select="."/>
</latest>
</xsl:otherwise>
</xsl:choose>
</xsl:copy>
</xsl:for-each>
<xsl:call-template name="missing">
<xsl:with-param name="ratingEntity" select="$ratingEntity[$ID ne UUID]" />
</xsl:copy>
</xsl:template>
<xsl:template name="missing">
<xsl:param name="ratingEntity"/>
<entity>
<xsl:copy-of select="$ratingEntity"/>
</entity>
</xsl:template>
<xsl:template name="latest">
<xsl:param name="ratingEntity"/>
<xsl:copy-of select="$ratingEntity except (UUID, issuer)"/>
</xsl:template>

I think you simply want
<xsl:template match="credit">
<xsl:copy>
<xsl:for-each-group select="entity, doc('/credit-rating.xml')/ratings/rating" group-by="UUID">
<entity>
<xsl:apply-templates select="* except (date, rating)"/>
<lastest>
<xsl:variable name="max-date" select="max(current-group()/xs:date(date))"/>
<date>{$max-date}</date>
<rating>{current-group()[date = $max-date]/rating}</rating>
</lastest>
</entity>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>

Related

How to match and merge XML in database?

I have below mixed documents in one collection.
B3:
<creditRisk>
<characteristic>
<score>
<LID>C230</LID>
<SPID>129587</SPID>
<Sector>Finance and Insurance</Sector>
</score>
<score>
<LID>C177</LID>
<SPID>360720</SPID>
<Sector>Mining and Oil and Gas Extraction</Sector>
</score>
</characteristic>
</creditRisk>
B4:
<creditRisk>
<pit>
<score>
<LID>C230</LID>
<SPID>129587</SPID>
<LTV>1.4689503</LTV>
<LGD>0.5995806998</LGD>
<Logarithm>-0.915243031</Logarithm>
</score>
<score>
<LID>C177</LID>
<SPID>360720</SPID>
<LTV>1.524224737</LTV>
<LGD>0.8989534312</LGD>
<Logarithm>-2.292173791</Logarithm>
</score>
</pit>
</creditRisk>
At the moment to simplify the problem, I need to merge pit/score#B4 when its SPID equals to characteristic/score/SPID#B3 inside MarkLogic. 
Expected Output:
<characteristic>
<score>
<default>
<LID>C230</LID>
<SPID>129587</SPID>
<LTV>1.4689503</LTV>
<LGD>0.5995806998</LGD>
<Logarithm>-0.915243031</Logarithm>
</default>
<LID>C230</LID>
<SPID>129587</SPID>
<Sector>Finance and Insurance</Sector>
</score>
<score>
<default>
<LID>C177</LID>
<SPID>360720</SPID>
<LTV>1.524224737</LTV>
<LGD>0.8989534312</LGD>
<Logarithm>-2.292173791</Logarithm>
</default>
<LID>C177</LID>
<SPID>360720</SPID>
<Sector>Mining and Oil and Gas Extraction</Sector>
</score>
</characteristic>
We are facing issue. My xsl comes out all blank results.
<xsl:template match="characteristic">
<characteristic>
<xsl:call-template name="scoreSPID">
<xsl:with-param name="characterScore" select="score"/>
</xsl:call-template>
</characteristic>
</xsl:template>
<xsl:template name="scoreSPID">
<xsl:param name="characterScore"/>
<xsl:for-each select="$characterScore">
<xsl:variable name="spid" select="SPID"/>
<score>
<xsl:for-each select="/creditRisk/pit/score[SPID eq $spid]">
<default>
<xsl:copy-of select="./node()"/>
</default>
<xsl:copy-of select="node()"/>
</xsl:for-each>
</score>
</xsl:for-each>
</xsl:template>
<xsl:template match="node()">
<xsl:apply-templates/>
</xsl:template>
How can I get the match/merge work in my xsl? Do note B3 and B4 are different dokuments in the same database.
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fn="http://www.w3.org/2005/xpath-functions"
xmlns:cts="http://marklogic.com/cts" xmlns:fff="schema://fc.fasset/functions"
exclude-result-prefixes="#all" version="2.0">
<xsl:function name="fff:mergeModelI">
<xsl:param name="characterScore"/>
<xsl:for-each select="$characterScore">
<xsl:variable name="spid" select="SPID"/>
<xsl:variable name="query"
select="cts:path-range-query('/creditRisk/pit/score/SPID', '=', $spid)"/>
<xsl:variable name="model"
select="cts:search(fn:collection('collection-name')/creditRisk/pit/score, $query)"/>
<xsl:choose>
<xsl:when test="exists($model)">
<score>
<matched>merged</matched>
<default>
<xsl:copy-of select="$model/node()"/>
</default>
<xsl:copy-of select="node()"/>
</score>
</xsl:when>
<xsl:otherwise/>
</xsl:choose>
</xsl:for-each>
</xsl:function>
<xsl:template match="characteristic">
<characteristic>
<xsl:sequence select="fff:mergeModelI(score)"/>
</characteristic>
</xsl:template>
<xsl:template match="node()">
<xsl:apply-templates/>
</xsl:template>
</xsl:transform>
If you are looking at continuous integration, the matched/merged document should be ingested in another database with distinct collection(s).
The aforesaid and data governance/auditing necessitate the match/merge operation tracking. Here I tag <matched>merged</matched> as a recourse. The matched/merged document can be populated in another database based on the tag. You can design more comprehensive canonicalization to suit your needs.
In the predicate filter for the for-each:
<xsl:for-each select="/creditRisk/pit/score[SPID eq spid]">
you want to filter where the SPID is equal to the variable $spid. Without the $ it is looking for a sibling element spid (which doesn't exist).
It should be:
<xsl:for-each select="/creditRisk/pit/score[SPID eq $spid]">

Better way to cycle xsl:for-each letter of the alphabet?

I have a long XML file from which I ned to pull out book titles and other information, then sort it alphabetically, with a separator for each letter. I also need a section for items that don't begin with a letter, say a number or symbol. Something like:
#
1494 - hardcover, $9.99
A
After the Sands - paperback, $24.95
Arctic Spirit - hardcover, $65.00
B
Back to the Front - paperback, $18.95
…
I also need to create a separate list of authors, created from the same data but showing different kinds of information.
How I'm currently doing it
This is simplified, but I basically have this same code twice, once for titles and once for authors. The author version of the template works with different elements and does different things with the data, so I can't use the same template.
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'#'" />
</xsl:call-template>
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'A'" />
</xsl:call-template>
…
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'Z'" />
</xsl:call-template>
<xsl:template name="BIP-letter">
<xsl:param name="letter" />
<xsl:choose>
<xsl:when test="$letter = '#'">
<xsl:text>#</xsl:text>
<xsl:for-each select="//Book[
not(substring(Title,1,1) = 'A') and
not(substring(Title,1,1) = 'B') and
…
not(substring(Title/,1,1) = 'Z')
]">
<xsl:sort select="Title" />
<xsl:appy-templates select="Title" />
<!-- Add other relevant data here -->
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$letter" />
<xsl:for-each select="//Book[substring(Title,1,1) = $letter]">
<xsl:sort select="Title" />
<xsl:appy-templates select="Title" />
<!-- Add other relevant data here -->
</xsl:for-each>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
My questions
The code above works just fine, but:
Manually cycling through each letter gets very long, especially having to do it twice. Is there a way to simplify that? Something like a <xsl:for-each select="[A-Z]"> that I could use to set the parameter when calling the template?
Is there a simpler way to select all titles that don't begin with a letter? Something like //Book[not(substring(Title,1,1) = [A-Z])?
There may be cases where the title or author name starts with a lowercase letter. In the code above, they would get grouped with under the # heading, rather than with the actual letter. The only way I can think to accommodate that—doing it manually—would significantly bloat up the code.
This solution answers all questions asked:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="vLowercase" select="'abcdefghijklmnopqrstuvuxyz'"/>
<xsl:variable name="vUppercase" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:variable name="vDigits" select="'0123456789'"/>
<xsl:key name="kBookBy1stChar" match="Book"
use="translate(substring(Title, 1, 1),
'abcdefghijklmnopqrstuvuxyz0123456789',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ##########'
)"/>
<xsl:template match="/*">
<xsl:apply-templates mode="firstInGroup" select=
"Book[generate-id()
= generate-id(key('kBookBy1stChar',
translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)
)[1]
)
]">
<xsl:sort select="translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Book" mode="firstInGroup">
<xsl:value-of select="'
'"/>
<xsl:value-of select="translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)"/>
<xsl:apply-templates select=
"key('kBookBy1stChar',
translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)
)">
<xsl:sort select="Title"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Book">
<xsl:value-of select="'
'"/>
<xsl:value-of select="concat(Title, ' - ', Binding, ', $', price)"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the following xml document (none provided in the question!):
<Books>
<Book>
<Title>After the Sands</Title>
<Binding>paperback</Binding>
<price>24.95</price>
</Book>
<Book>
<Title>Cats Galore: A Compendium of Cultured Cats</Title>
<Binding>hardcover</Binding>
<price>5.00</price>
</Book>
<Book>
<Title>Arctic Spirit</Title>
<Binding>hardcover</Binding>
<price>65.00</price>
</Book>
<Book>
<Title>1494</Title>
<Binding>hardcover</Binding>
<price>9.99</price>
</Book>
<Book>
<Title>Back to the Front</Title>
<Binding>paperback</Binding>
<price>18.95</price>
</Book>
</Books>
the wanted, correct result is produced:
#
1494 - hardcover, $9.99
A
After the Sands - paperback, $24.95
Arctic Spirit - hardcover, $65.00
B
Back to the Front - paperback, $18.95
C
Cats Galore: A Compendium of Cultured Cats - hardcover, $5.00
Explanation:
Use of the Muenchian method for grouping
Use of the standard XPath translate() function
Using mode to process the first book in a group of books starting with the same (case-insensitive) character
Using <xsl:sort> to sort the books in alphabetical orser
The most problematic part is this:
I also need a section for items that don't begin with a letter, say a number or symbol.
If you have a list of all possible symbols that an item can begin with, then you can simply use translate() to convert them all to the # character. Otherwise it gets more complicated. I would try something like:
XSLT 1.0 (+ EXSLT node-set())
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:key name="book" match="Book" use="index" />
<xsl:template match="/Books">
<!-- first-pass: add index char -->
<xsl:variable name="books-rtf">
<xsl:for-each select="Book">
<xsl:copy>
<xsl:copy-of select="*"/>
<index>
<xsl:variable name="index" select="translate(substring(Title, 1, 1), 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" />
<xsl:choose>
<xsl:when test="contains('ABCDEFGHIJKLMNOPQRSTUVWXYZ', $index)">
<xsl:value-of select="$index"/>
</xsl:when>
<xsl:otherwise>#</xsl:otherwise>
</xsl:choose>
</index>
</xsl:copy>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="books" select="exsl:node-set($books-rtf)/Book" />
<!-- group by index char -->
<xsl:for-each select="$books[count(. | key('book', index)[1]) = 1]">
<xsl:sort select="index"/>
<xsl:value-of select="index"/>
<xsl:text>
</xsl:text>
<!-- list books -->
<xsl:for-each select="key('book', index)">
<xsl:sort select="Title"/>
<xsl:value-of select="Title"/>
<xsl:text> - </xsl:text>
<xsl:value-of select="Binding"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="Price"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
However, this still leaves the problem of items that begin with a diacritic, e.g. "Österreich" or say a Greek letter. Under this method they too will be clumped under #.
Unfortunately, the only good solution for this is to move to XSLT 2.0.
Demo: https://xsltfiddle.liberty-development.net/jyRYYjj/2

Transforming Fixed-Length to XML in XSLT

I need to transform an fixed-length file to XML in XSLT v2.0. I saw other references but I can't apply in my XSLT. If for example, I have a text file like this:
UHL1 2016-999999 000000001 DAILY 001
ITNCC609890989099ITNCC463374755000010000.00 SANTANDER CONSUMERBOA-t-1111111111 Bank of America 2016-
ITNCC463374755017ITNCC463374755000010000.00 CONTRA SANTANDER CONSUMER 2016-
UTL110000.00 10000.00 00000010000001
The 1st line with UHL is a Header Record, the 2nd line is a Detail1 Record, 3rd line is a Detail2 Record and the last line is a Trailer Record.
I need to generate an XML file like this:
<BACSRecord>
<Header>
<Item1>UHL</Item1>
<Item2>1</Item2>
<Item3/>
<Item4>2016-</Item4>
<Item5>999999</Item5>
<Item6/>
<Item7>00</Item7>
<Item8>000000</Item8>
<Item9>1 DAILY </Item9>
<Item10>001</Item10>
<Item11/>
<Item12/>
<Item13/>
<Item14/>
</Header>
<Transaction>
<Detail1>
<Item1>ITNCC6</Item1>
<Item2>09890989</Item2>
<Item3>0</Item3>
<Item4>99</Item4>
<Item5>ITNCC4</Item5>
<Item6>63374755</Item6>
<Item7>0000</Item7>
<Item8>10000.00 </Item8>
<Item9>SANTANDER CONSUMER</Item9>
<Item10>BOA-t-1111111111 </Item10>
<Item11>Bank of America </Item11>
<Item12> 2016-</Item12>
</Detail1>
<Detail2>
<Item1>ITNCC4</Item1>
<Item2>63374755</Item2>
<Item3>0</Item3>
<Item4>17</Item4>
<Item5>ITNCC4</Item5>
<Item6>63374755</Item6>
<Item7>0000</Item7>
<Item8>10000.00 </Item8>
<Item9/>
<Item10>CONTRA</Item10>
<Item11/>
<Item12>SANTANDER CONSUMER</Item12>
<Item13> 2016-</Item13>
</Detail2>
</Transaction>
<Trailer>
<Item1>UTL</Item1>
<Item2>1</Item2>
<Item3>10000.00 </Item3>
<Item4>10000.00 </Item4>
<Item5>0000001</Item5>
<Item6>0000001</Item6>
<Item7/>
<Item8/>
</Trailer>
Is it possible to do it in XSLT?
Thank you.
Sure, something like this:
<xsl:template name="main">
<BACSRecord>
<xsl:variable name="lines" as="xs:string*" select="tokenize(unparsed-text('data.txt'), '\n')">
<Header>
<xsl:sequence select="f:header($lines[1])"/>
</Header>
<Transaction>
<xsl:for-each select="subsequence($lines, 2)">
<xsl:element name="Details{position()}">
<xsl:sequence select="f:details(.)"/>
</xsl:element>
</Transaction>
<Trailer>
<xsl:sequence select="f:trailer($lines[last()])"/>
</Header>
</xsl:template>
<xsl:function name="f:details" as="element(*)">
<xsl:param name="line" as="xs:string"/>
<xsl:sequence select="f:split(., (6,8,1,2,6,8,4,....))"/>
</xsl:function>
<xsl:function name="f:split" as="element(*)">
<xsl:param name="line" as="xs:string"/>
<xsl:param name="widths as="xs:integer*"/>
<xsl:for-each select="1 to count($widths)">
<xsl:element name="Item{.}">
<xsl:value-of select="subtring($line, sum(subsequence($widths, 1, .-1)), $widths[current()]"/>
</xsl:element>
</xsl:for-each>
</xsl:function>

Generic XSLT to do XML to CSV - almost there, but stuck

I have gathered bits and pieces of this XSLT from these forums. I'm trying to put them altogether to create a single, generic XSLT that can be used to convert XML to CSV by specifying the path to the nodes that should be included in the CSV file.
I have three things that I still can't figure out after about 10 hours of messing with it.
I want to iterate over each column named in csv:columns. During each iteration, I need to extract and store the text() of the column. I think this is the way to iterate, but want to make sure:
<xsl:for-each select="document('')/*/csv:columns/*">
Once I have the text() from the column, I need to put that into the columnname variable in such a way that it works when it is used with getNodeValue.
I was unable to set columnname using variable. If I didn't hard-code the value (surrounded by apostrophes), I could not get it to work. This is why I have the following line in the code:
<xsl:variable name="columnname" select="'location/city'" />
I want to pass the result of getNodeValue into quotevalue so that the result is properly quoted.
The XSLT:
<?xml version="1.0"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:csv="csv:csv" xpath-default-namespace="http://nowhere/" >
<xsl:output method="text" encoding="utf-8" />
<xsl:strip-space elements="*" />
<xsl:variable name="delimiter" select="','" />
<csv:columns>
<column>title</column>
<column>location/city</column>
</csv:columns>
<xsl:template match="job">
<xsl:value-of select="concat(#id, ',')"/>
<!-- #1 I WANT TO LOOP THROUGH ALL OF THE CSV COLUMNS HERE -->
<!-- #2 How do I put the text into the variable 'columnname' variable so that it works with getNodeValue? -->
<xsl:variable name="columnname" select="'location/city'" />
<xsl:variable name="vXpathExpression" select="$columnname"/>
<xsl:call-template name="getNodeValue">
<xsl:with-param name="pExpression" select="$vXpathExpression"/>
</xsl:call-template>
<!-- #3 After getNodeValue gets the value, I want to send that value into 'quotevalue' -->
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template name="getNodeValue">
<xsl:param name="pExpression"/>
<xsl:param name="pCurrentNode" select="."/>
<xsl:choose>
<xsl:when test="not(contains($pExpression, '/'))">
<xsl:value-of select="$pCurrentNode/*[name()=$pExpression]"/>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="getNodeValue">
<xsl:with-param name="pExpression"
select="substring-after($pExpression, '/')"/>
<xsl:with-param name="pCurrentNode" select=
"$pCurrentNode/*[name()=substring-before($pExpression, '/')]"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="quotevalue">
<xsl:param name="value"/>
<xsl:choose>
<!-- Quote the value if required -->
<xsl:when test="contains($value, '"')">
<xsl:variable name="x" select="replace($value, '"', '""')"/>
<xsl:value-of select="concat('"', $x, '"')"/>
</xsl:when>
<xsl:when test="contains($value, $delimiter)">
<xsl:value-of select="concat('"', $value, '"')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$value"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Sample XML
<?xml version="1.0" encoding="utf-8"?>
<positionfeed
xmlns="http://nowhere/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
version="2006-04">
<job id="2830302">
<employer>Acme</employer>
<title>Manager</title>
<description>Full time</description>
<postingdate>2016-09-15T23:12:13Z</postingdate>
<location>
<city>Los Angeles</city>
<state>California</state>
</location>
</job>
<job id="2830303">
<employer>Acme</employer>
<title>Clerk, evenings</title>
<description>Part time</description>
<postingdate>2016-09-15T23:12:13Z</postingdate>
<location>
<city>Albany</city>
<state>New York</state>
</location>
</job>
</positionfeed>
The current output using the XSLT I provided
2830302,Los Angeles
2830303,Albany
The output if the XSLT works as desired
2830302,Manager,Los Angeles
2830303,"Clerk, evenings",Albany
Solution (many thanks to Tim's help below)
<?xml version="1.0"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:csv="csv:csv" xpath-default-namespace="http://www.job-search-engine.com/add-jobs/positionfeed-namespace/" >
<xsl:output method="text" encoding="utf-8" />
<xsl:strip-space elements="*" />
<!-- Set the value of the delimiter character -->
<xsl:variable name="delimiter" select="','" />
<!-- The name of the node that contains the column values -->
<xsl:param name="containerNodeName" select="'job'"/>
<!-- All nodes that should be ignored during processing -->
<xsl:template match="source|feeddate"/>
<!-- The names of the nodes to be included in the CSV file -->
<xsl:variable name="columns" as="element()*">
<column header="Title">title</column>
<column header="Category">category</column>
<column header="Description">description</column>
<column header="PostingDate">postingdate</column>
<column header="URL">joburl</column>
<column header="City">location/city</column>
<column header="State">location/state</column>
</xsl:variable>
<!-- ************** DO NOT TOUCH BELOW **************** -->
<!-- ************** DO NOT TOUCH BELOW **************** -->
<!-- ************** DO NOT TOUCH BELOW **************** -->
<!-- ************** DO NOT TOUCH BELOW **************** -->
<!-- ************** DO NOT TOUCH BELOW **************** -->
<!-- Warn about unmatched nodes -->
<xsl:template match="*">
<xsl:message terminate="no">
<xsl:text>WARNING: Unmatched element: </xsl:text>
<xsl:value-of select="name()"/>
</xsl:message>
<xsl:apply-templates/>
</xsl:template>
<!-- Generate the column headers -->
<xsl:template match="//*[*[local-name()=$containerNodeName]]">
<xsl:value-of select="'Id'"/>
<xsl:value-of select="$delimiter"/>
<xsl:for-each select="$columns/#header">
<xsl:variable name="colname" select="." />
<xsl:value-of select="$colname"/>
<xsl:if test="position() != last()">
<xsl:value-of select="$delimiter"/>
</xsl:if>
</xsl:for-each>
<xsl:text>
</xsl:text>
<xsl:apply-templates />
</xsl:template>
<!-- Generate the rows of column data -->
<xsl:template match="//*[local-name()=$containerNodeName]">
<!-- TODO: Handle attributes generically -->
<xsl:value-of select="#id"/>
<xsl:variable name="container" select="." />
<xsl:for-each select="$columns">
<xsl:value-of select="$delimiter"/>
<xsl:variable name="vXpathExpression" select="."/>
<xsl:call-template name="getQuotedNodeValue">
<xsl:with-param name="pCurrentNode" select="$container"/>
<xsl:with-param name="pExpression" select="$vXpathExpression"/>
</xsl:call-template>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template name="getQuotedNodeValue">
<xsl:param name="pExpression"/>
<xsl:param name="pCurrentNode" select="."/>
<xsl:choose>
<xsl:when test="not(contains($pExpression, '/'))">
<xsl:variable name="result" select="$pCurrentNode/*[name()=$pExpression]"/>
<xsl:call-template name="quotevalue">
<xsl:with-param name="value" select="$result"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="getQuotedNodeValue">
<xsl:with-param name="pExpression" select="substring-after($pExpression, '/')"/>
<xsl:with-param name="pCurrentNode" select= "$pCurrentNode/*[name()=substring-before($pExpression, '/')]"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="quotevalue">
<xsl:param name="value"/>
<xsl:choose>
<xsl:when test="contains($value, '"')">
<!-- Quote the value and escape the double-quotes -->
<xsl:variable name="x" select="replace($value, '"', '""')"/>
<xsl:value-of select="concat('"', $x, '"')"/>
</xsl:when>
<xsl:otherwise>
<!-- Quote the value -->
<xsl:value-of select="concat('"', $value, '"')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Sample data to demonstrate solution
<?xml version="1.0" encoding="utf-8"?>
<positionfeed
xmlns="http://www.job-search-engine.com/add-jobs/positionfeed-namespace/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.job-search-engine.com/add-jobs/positionfeed-namespace/ http://www.job-search-engine.com/add-jobs/positionfeed.xsd"
version="2006-04">
<source>Casting360</source>
<feeddate>2016-11-11T21:48:34Z</feeddate><job id="1363612">
<employer>Casting360</employer>
<title>The Robert Irvine Show Is Seeking Guests</title>
<category>Reality TV</category>
<description>TV personality ROBERT IRVINE (Restaurant Impossible) is seeking guests looking for solutions to their unique problems to share their stories on his show!
Our next show is Thursday, September 22nd in LA. If you're not in LA we will provide your airfare, hotel, car service, and per diem.
Please note: WE ARE NOT LOOKING FOR RESUMES; THIS IS NOT AN ACTING GIG. We are looking for real people to share their stories!
*appearance fee (TBD)
If you or someone you know has a conflict that they need help resolving, WE WANT TO HEAR FROM YOU.
Please email tvgal.ri#gmail.com the following information:
Name
Phone number
Your story in 2-3 paragraphs
1-3 photos of yourself.</description>
<postingdate>2016-09-15T23:12:13Z</postingdate>
<joburl>http://casting360.com/lgj/8886644624?jobid=1363612&city=Los+Angeles&state=CA</joburl>
<location>
<nation>USA</nation>
<city>Los Angeles</city>
<state>California</state>
</location>
<jobsource>Casting360</jobsource>
</job><job id="1370302">
<employer>Casting360</employer>
<title>Photoshoot for Publication</title>
<category>Modeling</category>
<description>6 FEMALE Models are wanted for publication photoshoot.
If you're not in the NYC Vicinity (NY, Pa, Ct,) DO NOT REPLY because your response will be summarily ignored.
Chosen models will be given a 5 look photo shoot. The shoot will occur on location (outdoors) in highly public locations chosen both for it's convenience and scenery.
The 5 looks (outfits) will be pre-determined by our staff of items most outfits within a model's wardrobe.
THIS IS A TF (UNPAID) SHOOT. After the release of the magazine, the photos agreed upon from the shoot shall be given to the model (in digital format) for her to build her portfolio.
Chosen models will receive a 5 outfit photo shoot at no cost to them by a NY Fashion Photographer.As a result, chosen models not only receive a free photo shoot, but also become PUBLISHED MODELS featured in a magazine.
The model (Janeykay) centered in the photo attached (Please look at the attached photo) is a Casting360 member who not only received her photo shoot, not only is being featured in a magazine, but also made the cover becoming a Cover Model from her shoot with us.</description>
<postingdate>2016-10-03T00:34:43Z</postingdate>
<joburl>http://casting360.com/lgj/8886644624?jobid=1370302&city=New+York&state=NY</joburl>
<location>
<nation>USA</nation>
<city>New York</city>
<state>New York</state>
</location>
<jobsource>Casting360</jobsource>
</job><job id="1370962">
<employer>Casting360</employer>
<title>Actresses Needed for "Red Shore", Action Film</title>
<category>Acting</category>
<description>CASTING (non-union)
We are a New Independent company looking to shoot our first feature. We are currently looking to fill two Major roles.
Female/African American, Hispanic, Asian, Pacific Islander/ 5'5-5'10/ Age Late 30's-Early 40's.
Project description: A long standing feud between two best friends turned enemies escalates over a valuable Diamond on display in a New York City Museum. With the stakes high they each seek the help of both friends and strangers to settle their feud once and for all.
Please note this is a non-paid project.
Fight training will be provided for free.
Please email including age and height in your e-mail.
Those selected will be invited to our audition.</description>
<postingdate>2016-10-03T14:18:20Z</postingdate>
<joburl>http://casting360.com/lgj/8886644624?jobid=1370962&city=New+York&state=NY</joburl>
<location>
<nation>USA</nation>
<city>New York</city>
<state>New York</state>
</location>
<jobsource>Casting360</jobsource>
</job>
</positionfeed>
As you are using XSLT 2.0, you could define your columns in a variable like so:
<xsl:variable name="columns" as="element()*">
<column>title</column>
<column>location/city</column>
</xsl:variable>
Then you can just iterate over them with a simple statement
<xsl:for-each select="$columns">
But the problem you may be having is that within this xsl:for-each you have changed context. You are no longer positioned on a job element, but the column element, and you don't want your expression to be relative to that. You really need to swap back to being on the job element, which you can do simply by setting a variable reference to the job element before the xsl:for-each and then using that as a parameter to the named template:
<xsl:template match="job">
<xsl:value-of select="#id"/>
<xsl:variable name="job" select="." />
<xsl:for-each select="$columns">
<xsl:value-of select="$delimiter"/>
<xsl:variable name="vXpathExpression" select="."/>
<xsl:call-template name="getNodeValue">
<xsl:with-param name="pCurrentNode" select="$job"/>
<xsl:with-param name="pExpression" select="$vXpathExpression"/>
</xsl:call-template>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:template>
As for quoting the result; instead of doing just xsl:value-of simply call the quote template with the value as a parameter
<xsl:when test="not(contains($pExpression, '/'))">
<xsl:call-template name="quotevalue">
<xsl:with-param name="value" select="$pCurrentNode/*[name()=$pExpression]" />
</xsl:call-template>
</xsl:when>
EDIT: If you want a header row of column names, you would have to match the parent of the job node, and then just output the values of the $column variable
<xsl:template match="*[job]">
<xsl:value-of select="$columns" separator="," />
<xsl:text>
</xsl:text>
<xsl:apply-templates />
</xsl:template>
Or maybe this if you didn't want the full path
<xsl:value-of select="$columns/(tokenize(., '/')[last()])" separator="," />
Or you could extend your columns variable to have the header text
<xsl:variable name="columns" as="element()*">
<column header="Title">title</column>
<column header="City">location/city</column>
</xsl:variable>
Then you would do this...
<xsl:value-of select="$columns/#header" separator="," />

Generate XPath using XPath

I have two separate types of XML documents (one is a.xml & the other is b.xml). Document a.xml, is my main source document, on which I have to run queries. Document b.xml contains all possible information to fetch records from a.xml.
Document: «a.xml»
<rs>
<r id="r1">
<f0>typeA</f0>
<f1>contains value1, value2 and value3</f1>
</r>
<r id="r2">
<f0>typeB</f0>
<f1>contains value4 and value7</f1>
</r>
<r id="r3">
<f0>typeA</f0>
<f1>contains value2 and value5</f1>
</r>
<r id="r4">
<f0>typeC</f0>
<f1>contains value1 and value6</f1>
</r>
<r id="r5">
<f0>typeA</f0>
<f1>contains value5</f1>
</r>
<r id="r6">
<f0>typeC</f0>
<f1>contains value1, value2 and value3</f1>
</r>
</rs>
Document: «b.xml»
<?xml version="1.0"?>
<qs>
<q id="q1">
<i0>typeA</i0>
<i1>value1|value2|value3</i1>
<i2>value18|value35</i2>
<i3>value1|value7</i3>
</q>
<q id="q2">
<i0>typeB</i0>
<i1>value2|value7</i1>
<i2>value9|value20</i2>
<i3>value4</i3>
</q>
</qs>
Now I like to generate dynamic XPath selector strings based on the values of b.xml to be stored in Document c.xml. And it would look like:
Document c.xml
<xps>
<xp id="q1">
<t1>/rs/r[contains(f0,'typeA')
and contains(f1,'value1')
and contains(f1,'value2')
and contains(f1,'value3')]</t1>
<t2>/rs/r[contains(f0,'typeA')
and contains(f1,'value18')
and contains(f1,'value35')]</t2>
<t3>/rs/r[contains(f0,'typeA')
and contains(f1,'value1')
and contains(f1,'value7')]</t3>
</xp>
<xp id="q2">
<t1>/rs/r[contains(f0,'typeB')
and contains(f1,'value2')
and contains(f1,'value7')]</t1>
<t2>/rs/r[contains(f0,'typeA')
and contains(f1,'value9')
and contains(f1,'value20')]</t2>
<t3>/rs/r[contains(f0,'typeA')
and contains(f1,'value4')]</t3>
</xp>
</xps>
If somebody here having any idea, how to do that job in XSLT version 1.0. Thanks in advance.
Here is a solution in XSLT 1.0:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" encoding="utf-8" indent="yes"/>
<xsl:template match="#*|text()" />
<xsl:template match="/">
<xps>
<xsl:apply-templates/>
</xps>
</xsl:template>
<xsl:template match="q">
<xp id="{#id}">
<xsl:apply-templates/>
</xp>
</xsl:template>
<xsl:template match="*[starts-with(name(), 'i')][not(self::i0)]">
<xsl:element name="t{substring-after(name(), 'i')}">
<xsl:text>/rs/r[contains(f0, '</xsl:text>
<xsl:value-of select="preceding-sibling::i0"/>
<xsl:text>')</xsl:text>
<xsl:call-template name="more-conditions">
<xsl:with-param name="list" select="."/>
</xsl:call-template>
<xsl:text>]</xsl:text>
</xsl:element>
</xsl:template>
<xsl:template name="more-conditions">
<xsl:param name="list"/>
<xsl:param name="delimiter" select="'|'"/>
<xsl:choose>
<xsl:when test="contains($list, $delimiter)">
<xsl:call-template name="more-conditions">
<xsl:with-param name="list" select="substring-before($list, $delimiter)"/>
</xsl:call-template>
<xsl:call-template name="more-conditions">
<xsl:with-param name="list" select="substring-after($list, $delimiter)"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:text> and contains(f1, '</xsl:text>
<xsl:value-of select="$list"/>
<xsl:text>')</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Applied to your input document, it produces the following output:
<xps>
<xp id="q1">
<t1>/rs/r[contains(f0, 'typeA') and contains(f1, 'value1') and contains(f1, 'value2') and contains(f1, 'value3')]</t1>
<t2>/rs/r[contains(f0, 'typeA') and contains(f1, 'value18') and contains(f1, 'value35')]</t2>
<t3>/rs/r[contains(f0, 'typeA') and contains(f1, 'value1') and contains(f1, 'value7')]</t3>
</xp>
<xp id="q2">
<t1>/rs/r[contains(f0, 'typeB') and contains(f1, 'value2') and contains(f1, 'value7')]</t1>
<t2>/rs/r[contains(f0, 'typeB') and contains(f1, 'value9') and contains(f1, 'value20')]</t2>
<t3>/rs/r[contains(f0, 'typeB') and contains(f1, 'value4')]</t3>
</xp>
</xps>
I added some of the whitespace. You can modify the transform based on your needs, but this should get you started.
You cannot use a variable as an xpath selector in xslt version 1, however there are likely other ways you might accomplish this task. If you provide an idea of the problem rather than your intended solution people might be able to help :)