pattern for generating JSON output - xslt-3.0

I'm trying to map XML to JSON using XSLT 3.0
my broad plan is to take the input, map it to some elements in memory, and then map that to 'map's and 'array's to by applying templates and then letting the XSLT serialise that as JSON.
Here is my initial effort:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs"
xmlns:map="http://www.w3.org/2005/xpath-functions/map"
version="3.0">
<xsl:output method="json" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<xsl:variable name="dsl" as="element()">
<epg lastBuildDate="10/4/2019 9:46:00 AM">
</epg>
</xsl:variable>
<xsl:variable name="output">
<xsl:apply-templates select="$dsl" mode="interpret"/>
</xsl:variable>
<xsl:sequence select="$output"/>
</xsl:template>
<xsl:template match="epg" mode="interpret">
<xsl:sequence select="map {
'lastBuildDate' : #lastBuildDate
}"/>
</xsl:template>
</xsl:stylesheet>
sadly I get
Cannot add a map to an XDM node tree
in the 'interpret' template.

The error occurs with
<xsl:variable name="output">
<xsl:apply-templates select="$dsl" mode="interpret"/>
</xsl:variable>
as that is an implicit use of
<xsl:variable name="output">
<xsl:document>
<xsl:apply-templates select="$dsl" mode="interpret"/>
</xsl:document>
</xsl:variable>
While maps can contain nodes as values, nodes can't contain maps.
For that simple example it should work to use
<xsl:variable name="output" as="item()*">
<xsl:apply-templates select="$dsl" mode="interpret"/>
</xsl:variable>
or even
<xsl:variable name="output" as="item()">
<xsl:apply-templates select="$dsl" mode="interpret"/>
</xsl:variable>
or
<xsl:variable name="output" as="map(*)">
<xsl:apply-templates select="$dsl" mode="interpret"/>
</xsl:variable>

this works, I'll mark the other answer as correct though there is a 2nd issue where you have to 'cast' the attribute to a string
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs"
xmlns:map="http://www.w3.org/2005/xpath-functions/map"
version="3.0">
<xsl:output method="json" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<xsl:variable name="dsl" as="element()">
<epg lastBuildDate="10/4/2019 9:46:00 AM">
</epg>
</xsl:variable>
<xsl:variable name="output" as="map(*)*">
<xsl:apply-templates select="$dsl" mode="interpret"/>
</xsl:variable>
<xsl:sequence select="$output"/>
</xsl:template>
<xsl:template match="epg" as="map(*)" mode="interpret">
<xsl:sequence select="map {
'lastBuildDate' : #lastBuildDate => xs:string()
}"/>
</xsl:template>
</xsl:stylesheet>

Related

xsltproc XML parser multi value

I'm using xsltproc to parse XML content into text content. Need to separate multiple values for one tag.
My XML:
<data xml:space="preserve" id="USER">
<c1>USER NAME</c1>
<c2>ADDRESS</c2>
<c3>DET</c3>
<c4>AILS</c4>
<c5>1001</c5>
<c5 a="2">2001</c5>
<c5 a="3">3001</c5>
<c5 a="4">401</c5>
<c5 a="5">5001</c5>
<c5 a="6">6001</c5>
<c6>1</c6>
<c7>20991231M0601</c7>
</data>
My xsl:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<!-- write out comma separated file -->
<xsl:template match="data">
<xsl:value-of select="c1"/>
<xsl:value-of select="','"/>
<xsl:value-of select="c2"/>
<xsl:value-of select="','"/>
<xsl:value-of select="c3"/>
<xsl:value-of select="','"/>
<xsl:value-of select="c4"/>
<xsl:value-of select="','"/>
<xsl:apply-templates select="c5"/>
<xsl:value-of select="','"/>
<xsl:value-of select="c6"/>
<xsl:value-of select="','"/>
<xsl:value-of select="c7"/>
<xsl:value-of select="','"/>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
Post execution,
My output comes like,
USER NAME,ADDRESS,DET,AILS,10012001300140150016001,1,20991231M0601,
But i wanted an output like
USER NAME,ADDRESS,DET,AILS,1001|2001|3001|401|5001|6001,1,20991231M0601,
With separator for the multi value.
How about:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="data">
<xsl:for-each select="c1|c2|c3|c4">
<xsl:value-of select="."/>
<xsl:text>,</xsl:text>
</xsl:for-each>
<xsl:for-each select="c5">
<xsl:value-of select="."/>
<xsl:if test="position()!=last()">|</xsl:if>
</xsl:for-each>
<xsl:text>,</xsl:text>
<xsl:value-of select="c6"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="c7"/>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>

Select part of String in XSLT

I want to select part of a String using xslt.
Input :
p:endnote_bl1
p:endnote_bl2
p:endnote_bl3
p:endnote_bl4
Output Should be :
endnote_bl1
endnote_bl2
endnote_bl3
endnote_bl4
Can I do this using split or something else? I am using XSLT 2.0
Use replace function
replace('p:endnote_bl1', '^[^:]*:', '')
You could do simply:
substring-after($string, ':')
There are a multitude of ways to do this.
This transformation shows four different solutions, the first two of which are XPath 1.0 (XSLT 1.0) solutions:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:variable name="vSolution1">
<xsl:apply-templates select="/*" mode="substring-after"/>
</xsl:variable>
<xsl:variable name="vSolution2">
<xsl:apply-templates select="/*" mode="substring"/>
</xsl:variable>
<xsl:variable name="vSolution3">
<xsl:apply-templates select="/*" mode="split"/>
</xsl:variable>
<xsl:variable name="vSolution4">
<xsl:apply-templates select="/*" mode="replace"/>
</xsl:variable>
<xsl:template match="/">
<xsl:copy-of select="$vSolution1"/>
==================
<xsl:copy-of select="$vSolution2"/>
==================
<xsl:copy-of select="$vSolution3"/>
==================
<xsl:copy-of select="$vSolution4"/>
</xsl:template>
<xsl:template match="node()|#*" mode="#all">
<xsl:copy>
<xsl:apply-templates select="node()|#*" mode="#current"/>
</xsl:copy>
</xsl:template>
<xsl:template match="s/text()" mode="substring-after">
<xsl:value-of select="substring-after(., 'p:')"/>
</xsl:template>
<xsl:template match="s/text()" mode="substring">
<xsl:value-of select="substring(., 3)"/>
</xsl:template>
<xsl:template match="s/text()" mode="split">
<xsl:value-of select="tokenize(., ':')[2]"/>
</xsl:template>
<xsl:template match="s/text()" mode="replace">
<xsl:value-of select="replace(., '^.+:', '')"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the XML document below (none was provided!):
<t>
<s>p:endnote_bl1</s>
<s>p:endnote_bl2</s>
<s>p:endnote_bl3</s>
<s>p:endnote_bl4</s>
</t>
We get four identical, correct results, each produced by one of the four solutions:
<t>
<s>endnote_bl1</s>
<s>endnote_bl2</s>
<s>endnote_bl3</s>
<s>endnote_bl4</s>
</t>
==================
<t>
<s>endnote_bl1</s>
<s>endnote_bl2</s>
<s>endnote_bl3</s>
<s>endnote_bl4</s>
</t>
==================
<t>
<s>endnote_bl1</s>
<s>endnote_bl2</s>
<s>endnote_bl3</s>
<s>endnote_bl4</s>
</t>
==================
<t>
<s>endnote_bl1</s>
<s>endnote_bl2</s>
<s>endnote_bl3</s>
<s>endnote_bl4</s>
</t>

Print xml nodes for each element in a single row

I need to print the category and hours values from all nodes in this xml as comma separated values in a single row through xslt -
XML
<?xml version="1.0" encoding="UTF-8"?>
<course>
<subcourse>
<code>ABC</code>
<name>REFCOURSE</name>
<date>Date</date>
<category>SDF</category>
<hours>7</hours>
</subcourse>
<subcourse>
<code>DEF</code>
<name>ORIGCOURSE</name>
<date>Date</date>
<category>UIT</category>
<hours>9</hours>
</subcourse>
</course>
Output needed -
SDF,7,UIT,9
By taking help from stakoverflow, here's what I've done so far -
<?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" version="2.0">
<xsl:param name="range-1-begin" select="4"/>
<xsl:param name="range-1-end" select="5"/>
<xsl:param name="range-2-begin" select="6"/>
<xsl:param name="range-2-end" select="7"/>
<xsl:output method="text" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="subcourse">
<info><xsl:apply-templates/></info>
</xsl:template>
<xsl:template match="subcourse">
<xsl:if test = "not(position()= 1)">
<xsl:text>,</xsl:text>
</xsl:if>
<xsl:value-of select="."/>
</xsl:template>
</xsl:stylesheet>
Output - ABCREFCOURSEDateSDF7,DEFORIGCOURSEDateUIT9
I need it to iterate through every subcourse and pick category and hours if exist. I could not find how to pick only category and hours.
<?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" version="2.0">
<xsl:template match="//subcource">
<xsl:if test="category">
<xsl:if test = "not(position()=1)">
<xsl:text>,</xsl:text>
</xsl:if>
<xsl:value-of select="category"/><xsl:text>,</xsl:text><xsl:value-of select="hours"/>
</xsl:if>
</xsl:template>
</xsl:stylesheet>

XSLT 2.0 : Split a string into comma separated values

I am new to XSLT and have a requirement where in i have to manipulate a string as below.
Input string = "12345"
Output expected ="12345,1234,123,12"
Can anybody help me to achieve this in XSLT 2.0
Here is some XSLT/XPath 2.0 approach:
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:mf="http://example.com/mf"
exclude-result-prefixes="xs mf"
version="2.0">
<xsl:function name="mf:sub-sequences" as="xs:string*">
<xsl:param name="input" as="xs:string"/>
<xsl:param name="min-length" as="xs:integer"/>
<xsl:sequence select="reverse(
for $length in $min-length to string-length($input)
return substring($input, 1, $length)
)"/>
</xsl:function>
<xsl:template name="main">
<xsl:variable name="s" select="'12345'"/>
<xsl:value-of select="mf:sub-sequences($s, 2)" separator=","/>
</xsl:template>
</xsl:stylesheet>
This should do the trick:
<xsl:template match="/">
<xsl:call-template name="minus-one">
<xsl:with-param name="input" select="'12345'"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="minus-one">
<xsl:param name="input"/>
<xsl:value-of select="$input"/>
<xsl:if test="string-length($input) gt 2"><xsl:text>,</xsl:text>
<xsl:call-template name="minus-one">
<xsl:with-param name="input" select="substring($input, 1, string-length($input) - 1)"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
Here is a more efficient solution than the currently accepted one that doesn't use the reverse() function:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:my="my:my">
<xsl:output method="text"/>
<xsl:function name="my:subSequences" as="xs:string*">
<xsl:param name="pString" as="xs:string"/>
<xsl:param name="pstartLength" as="xs:integer"/>
<xsl:sequence select=
"for $totalLength in string-length($pString),
$length in 1 to $totalLength -$pstartLength +1,
$revLength in $totalLength -$length +1
return
substring($pString, 1, $revLength)"/>
</xsl:function>
<xsl:template match="/">
<xsl:value-of select="my:subSequences('12345', 2)" separator=","/>
</xsl:template>
</xsl:stylesheet>
When this transformation is executed, the wanted, correct result is produced:
12345,1234,123,12
Explanation:
The XPath 2.0 W3C Spec defines that if the first argument of the to operator is greater than the second argument, then the resulting sequence is the empty sequense.
It is still possible to avoid this limitation and to construct a decreasing integer sequence, like this:
for $k in 0 to $big - $small
return
$big - $k
Using such expression is more efficient, especially for large sequences, than first constructing an increasing sequence and then reversing it with the reverse() function.
on
<string>12345</string>
the following xslt will produce the result 12345,1234,123,12
<?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="string">
<xsl:call-template name="doTheFunkeyMonkey">
<xsl:with-param name="data" select="."/>
</xsl:call-template>
</xsl:template>
<xsl:template name="doTheFunkeyMonkey">
<xsl:param name="data"/>
<xsl:value-of select="$data"/>
<xsl:if test="string-length($data) > 2">
<xsl:text>,</xsl:text>
<xsl:call-template name="doTheFunkeyMonkey">
<xsl:with-param name="data" select="substring($data,1,string-length($data)-1)"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>

how to handle array of strings in ".xsl" file?

I have an array of strings in .xsl file, now I have to use the each string separeted by spaces differently. How I can get the strings?
I have following array of strings:
strarray="hw.oh.xml hg.hd.gnl th.ik.lkj"
I have to get "hw.oh.xml" , "hg.hd.gnl" , "th.ik.lkj" strings separetaly to perform some operation on it.
How I can do that?
There are many ways to do this:
I. Using the XPath substring-before() and substring-after() functions:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:variable name="vStrArray" select="'hw.oh.xml hg.hd.gnl th.ik.lkj'"/>
<xsl:template match="/">
<xsl:value-of select="substring-before($vStrArray, ' ')"/>
<xsl:text>
</xsl:text>
<xsl:value-of select="substring-before(substring-after($vStrArray, ' '),' ')"/>
<xsl:text>
</xsl:text>
<xsl:value-of select="substring-after(substring-after($vStrArray, ' '),' ')"/>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on any XML document (not used), the wanted result (each item from the "array") is produced:
hw.oh.xml
hg.hd.gnl
th.ik.lkj
This method can quickly become overwhelmingly complex and is not recommended except for "arrays" of just 2-3 items.
II. Representing the "array" as an XML document in XSLT 1.0:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="my:my">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<my:array>
<item>hw.oh.xml</item>
<item>hg.hd.gnl</item>
<item>th.ik.lkj</item>
</my:array>
<xsl:variable name="vStrArray"
select="document('')/*/my:array/*"/>
<xsl:template match="/">
<xsl:value-of select="$vStrArray[1]"/>
<xsl:text>
</xsl:text>
<xsl:value-of select="$vStrArray[2]"/>
<xsl:text>
</xsl:text>
<xsl:value-of select="$vStrArray[3]"/>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on the same XML document (any), the wanted correct result is produced:
hw.oh.xml
hg.hd.gnl
th.ik.lkj
I recommend this method of representing an "array" -- for XSLT 1.0 applications.
III. XSLT 2.0 / XPath 2.0
Simply use a sequence of strings:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:variable name="vStrArray"
select="'hw.oh.xml', 'hg.hd.gnl', 'th.ik.lkj'"/>
<xsl:template match="/">
<xsl:sequence select=
"for $i in 1 to count($vStrArray)
return
concat($vStrArray[$i], '
')
"/>
</xsl:template>
</xsl:stylesheet>
Result:
hw.oh.xml
hg.hd.gnl
th.ik.lkj
UPDATE: The OP commented that he is stuck with the initial representation of space-separated values, contained in a single string.
IV. Convert the space-separated values string into an XML fragment for easy use.
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:variable name="vStrArray" select="'hw.oh.xml hg.hd.gnl th.ik.lkj'"/>
<xsl:variable name="vrtfDoc">
<xsl:call-template name="makeIndex">
<xsl:with-param name="pText" select="$vStrArray"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="vStrIndex" select="ext:node-set($vrtfDoc)/*"/>
<xsl:template match="/">
<xsl:value-of select="$vStrIndex[1]"/>
<xsl:text>
</xsl:text>
<xsl:value-of select="$vStrIndex[2]"/>
<xsl:text>
</xsl:text>
<xsl:value-of select="$vStrIndex[3]"/>
</xsl:template>
<xsl:template name="makeIndex">
<xsl:param name="pText"/>
<xsl:if test="string-length($pText)>0">
<item>
<xsl:value-of select=
"substring-before(concat($pText,' '), ' ')"/>
</item>
<xsl:call-template name="makeIndex">
<xsl:with-param name="pText" select=
"substring-after($pText,' ')"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
This transformation creates an XML fragment from the string in which every <item> element contains just one of the string values. Then its use is just as if it were an array.

Resources