Working with a imported JSON data with a relatively flat hierarchical tree structure.
In the current code I am applying attribute-sets during the creation of elements.
Is it possible to create all the elements first and after that apply the attribute-sets?
It seems "use-attribute-sets" is an attribute, thus need to be added onto a element to work.
There are no error messages in my current code.
I am just looking to see if possible to do things in certain sequence as described below.
The reason for this planned change is to handle a bigger data volume thus perform the parsing and creation of element first, and only after that step, perform a unified way of adding attributes through attribute-sets.
The sequence I have:
[1] Create attribute sets.
[2] Group element names.
[3] Parse JSON to XML map.
[4] Build element, using attribute-sets and extract key value
The sequence I would like to perform:
[1] Create attribute sets (same as above).
[2] Group element names (same as above).
[3] Parse JSON to XML map (same as above).
[4] Build element names with corresponding key (split of above bullet 4).
[5] Add attribute-set based on template match in the code (split of above bullet 4).
JSON:
<data>
{
"store": {
"pencils": 43,
"milk": 21,
"rulers": 12,
"beer": 17
}
}
</data>
XSL:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:inventory="http://www.example.org/1"
xmlns:item="http://www.example.org/2"
expand-text="yes"
>
<xsl:output method="xml" indent="yes"/>
<xsl:mode on-no-match="shallow-skip"/>
<!-- [1] Create attribute sets -->
<xsl:attribute-set name="group-office">
<xsl:attribute name="contextRef">office</xsl:attribute>
</xsl:attribute-set>
<!-- [2] Group element names-->
<xsl:param name="group-office">pencils, rulers</xsl:param>
<xsl:param name="attributes-for-group-office" select="tokenize($group-office, ',\s*')"/>
<!-- [3] Parse JSON to XML -->
<xsl:template match="data">
<inventory:store>
<xsl:apply-templates select="json-to-xml(.)/*"/>
</inventory:store>
</xsl:template>
<!-- [4] Build element, using attribute-sets and extract key value -->
<xsl:template match="*[#key = 'store']/*[#key = $attributes-for-group-office]">
<xsl:for-each select=".">
<xsl:element name="item:{#key}" use-attribute-sets="group-office">
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:for-each>
</xsl:template>
</xsl:transform>
Result
(current and how it should look like after sequence change of code):
<?xml version="1.0" encoding="UTF-8"?>
<inventory:store xmlns:inventory="http://www.example.org/1"
xmlns:item="http://www.example.org/2">
<item:pencils contextRef="office">43</item:pencils>
<item:rulers contextRef="office">12</item:rulers>
</inventory:store>
It is a rather artificial and not really easy separation as you can't inject an attribute set without creating an element so all you could do, if that helps, write a template with high-priority to select the element name and pass it on to a lower one that expects that name as a parameter and then do the real work, like before, to create the element with the attribute set:
<!-- [4] Extract key value for element name -->
<xsl:template match="*[#key = 'store']/*[#key = $attributes-for-group-office]" priority="10">
<xsl:next-match>
<xsl:with-param name="element-name" select="'item:' || #key"/>
</xsl:next-match>
</xsl:template>
<!-- [5] Build element and add attribute-set based on template match in the code -->
<xsl:template match="*[#key = 'store']/*[#key = $attributes-for-group-office]" priority="5">
<xsl:param name="element-name" required="yes"/>
<xsl:element name="{$element-name}" use-attribute-sets="group-office">
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:template>
I am not sure that separation makes any sense but for the time being I can't think of anything else if you really want two separate templates. Of course the mode based suggestion by Siebe with a temporary tree is also an option but needs that temporary tree; or you could use the above and instead of relying on priorities to ensure the order of processing use a mode and push the same node through, for me it feels like the artificial and difficult separation as before:
<!-- [4] Build element and extract key value -->
<xsl:template match="*[#key = 'store']/*[#key = $attributes-for-group-office]">
<xsl:apply-templates select="." mode="add-attribute-sets">
<xsl:with-param name="element-name" select="'item:' || #key"/>
</xsl:apply-templates>
</xsl:template>
<!-- [5] Build element and add attribute-set based on template match in the code -->
<xsl:template match="*[#key = 'store']/*[#key = $attributes-for-group-office]" mode="add-attribute-sets">
<xsl:param name="element-name" required="yes"/>
<xsl:element name="{$element-name}" use-attribute-sets="group-office">
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:template>
You can use modes, like this (Added some elements to make the wanted stages clear):
<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:inventory="http://www.example.org/1"
xmlns:item="http://www.example.org/2"
expand-text="yes"
>
<xsl:output method="xml" indent="yes"/>
<xsl:mode on-no-match="shallow-skip"/>
<xsl:mode name="useAttributeSet" on-no-match="shallow-skip"/>
<!-- [1] Create attribute sets -->
<xsl:attribute-set name="group-office">
<xsl:attribute name="contextRef">office</xsl:attribute>
</xsl:attribute-set>
<!-- [2] Group element names-->
<xsl:param name="group-office">pencils, rulers</xsl:param>
<xsl:param name="attributes-for-group-office" select="tokenize($group-office, ',\s*')"/>
<!-- [3] Parse JSON to XML -->
<xsl:template match="data">
<xsl:variable name="withoutAttributeSets">
<xsl:apply-templates select="json-to-xml(.)/*"/>
</xsl:variable>
<stages>
<stage>
<inventory:store>
<xsl:copy-of select="$withoutAttributeSets"/>
</inventory:store>
</stage>
<stage>
<inventory:store>
<xsl:apply-templates select="$withoutAttributeSets" mode="useAttributeSet"/>
</inventory:store>
</stage>
</stages>
</xsl:template>
<!-- [4] Build element names with corresponding key (split of above bullet 4). -->
<xsl:template match="*[#key = 'store']/*[#key = $attributes-for-group-office]">
<xsl:for-each select=".">
<xsl:element name="item:{#key}" >
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:for-each>
</xsl:template>
<!-- [5] Add attribute-set based on template match in the code (split of above bullet 4). -->
<xsl:template match="*[local-name() = $attributes-for-group-office]" mode="useAttributeSet">
<xsl:element name="{name()}" use-attribute-sets="group-office">
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:template>
</xsl:transform>
This wil give this result:
<?xml version="1.0" encoding="UTF-8"?>
<stages xmlns:inventory="http://www.example.org/1"
xmlns:item="http://www.example.org/2">
<stage>
<inventory:store>
<item:pencils>43</item:pencils>
<item:rulers>12</item:rulers>
</inventory:store>
</stage>
<stage>
<inventory:store>
<item:pencils contextRef="office">43</item:pencils>
<item:rulers contextRef="office">12</item:rulers>
</inventory:store>
</stage>
</stages>
Adjust it to your needs.
If you would like to reuse this [4]-stage some other way you also could save it like this:
<xsl:result-document href="stage-4.xml">
<inventory:store>
<xsl:copy-of select="$withoutAttributeSets"/>
</inventory:store>
</xsl:result-document>
Related
I am trying to convert XML to json using XSLT 3.0, lowercasing all keys and moving the first attribute, if any, as a JSON child. So given following (dummy) input XML:
<FOO id="1">
<BAR xy="2">
<SPAM>N</SPAM>
</BAR>
</FOO>
I am expecting
{
"foo" : {
"id" : "1",
"bar" : {
"xy" : "2",
"spam" : "N"
}
}
}
Using this XSLT:
<?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="http://www.w3.org/2005/xpath-functions"
exclude-result-prefixes="#all"
expand-text="yes"
version="3.0">
<xsl:output method="xml" indent='true' omit-xml-declaration='yes'/>
<xsl:template match="dummy">
<xsl:variable name="json-xml">
<xsl:apply-templates/>
</xsl:variable>
<xsl:value-of select="xml-to-json($json-xml, map { 'indent' : true() })"/>
</xsl:template>
<!--no children-->
<xsl:template match="*[not(*)]">
<string key="{lower-case(local-name())}">{.}</string>
</xsl:template>
<xsl:template match="*[*]">
<xsl:param name="key" as="xs:boolean" select="true()"/>
<map>
<xsl:if test="$key">
<xsl:attribute name="key" select="lower-case(local-name())"/>
</xsl:if>
<xsl:for-each-group select="*" group-by="node-name()">
<xsl:choose>
<xsl:when test="current-group()[2]">
<array key="{lower-case(local-name())}">
<xsl:apply-templates select="current-group()">
<xsl:with-param name="key" select="false()"/>
</xsl:apply-templates>
</array>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="current-group()">
<xsl:with-param name="key" select="true()"/>
</xsl:apply-templates>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</map>
</xsl:template>
</xsl:stylesheet>
I get (note the dummy pattern to skip json conversion),
<map xmlns="http://www.w3.org/2005/xpath-functions" key="foo">
<map key="bar">
<string key="spam">N</string>
</map>
</map>
Looks good to me but when I invoke the JSON conversion (by replacing dummy with /), I get:
{ "bar" :
{ "spam" : "N" } }
-> the foo node is gone.
I haven't figured out how to "move" the first attribute (arbitrary, could have any name) as a child node - if someone knows, appreciate a little snippet.
Lastly, not a big-deal but I am lowercasing keys in each template. Is it possible to do the transformation at once, either before in the source XML, or after templating, in the Result XML (before jsonifying) ?
See - https://xsltfiddle.liberty-development.net/jyfAiDC/2
(thanks btw to #Martin Honnen for this very useful tool !!)
You can use
<xsl:template match="/">
<xsl:variable name="json-xml">
<map>
<xsl:apply-templates/>
</map>
</xsl:variable>
<xsl:value-of select="xml-to-json($json-xml, map { 'indent' : true() })"/>
</xsl:template>
perhaps to get closer to what you might want. But I haven't understood what you want to do with attributes in the XML.
The key attribute only make sense for an element that represents an entry in a map, so there's no point including it in an element unless that element has a parent named map. You want another layer of map in your structure.
Thanks to Martin & Michael for the map wrapper tip. Agreed though it is an unnecessary level in tree.
For rendering a XML attribute as a child node - assuming there is one only -,
I added the following after the first test (conditional map attribute) in the template:
<xsl:if test="#*[1]">
<string key="{name(#*[1])}">{#*[1]}</string>
</xsl:if>
Lastly, for converting to lowercase all intermediary key attributes in one-go instead of individually in multiple templates, it would require I think parsing the result tree before passing on to the xml-to-json function.
Not worth it... but it would be a nice featured option in XSLT 4.0 (?) i.e. a new xml-to-json option force-key-case = lower/upper
I am trying to parse nested JSON to CSV, using XSLT transformation.
In this particular case each child object counting from "datasheet", e.g. "result-sheet" and "balance-sheet", should end up in one CSV file (output) each. Currently I am however just elaborating getting out "result-sheet" only.
I noticed that the content of arrays are getting merged togehter.
Data:
<data>
{
"datasheets": {
"result-sheet": {"bank": [1,3], "credit": [2,6]},
"balance-sheet": {"loans": [4,5], "inventory": [9,0]}
}
}
</data>
XSL:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:map="http://www.w3.org/2005/xpath-functions/map"
xmlns:array="http://www.w3.org/2005/xpath-functions/array"
>
<xsl:output method="text" indent="yes"/>
<xsl:mode on-no-match="shallow-skip"/>
<!-- Parse JSON to XML -->
<xsl:template match="data">
<xsl:apply-templates select="json-to-xml(.)"/>
</xsl:template>
<xsl:template match="*">
<h2>Result sheet</h2>
<xsl:text>
</xsl:text>
<xsl:value-of select="*/(string-join(head(*)/*/#key, ','), *!string-join(*, ','))" separator="
"/>
</xsl:template>
</xsl:stylesheet>
Result:
Result sheet
bank,credit
13,26
45,90
Wanted result:
bank,credit
1, 2,
3, 6
I don't quite understand which data you want to have in each line, the following templates creates a line using for-each-pair on each pair of fn:number elements in the two fn:array children of the fn:map with the #key being result-sheet:
<xsl:template match="*:map[#key = 'result-sheet']">
<xsl:value-of select="for-each-pair(*:array[1]/*:number, *:array[2]/*:number, function($n1, $n2) { $n1 || ', ' || $n2 })"
separator="
"/>
</xsl:template>
i would like to find the highest temperate of the month APRIL, the xml has other month too.
how can i code in my xsl to retrieve the highest temperate?
here is my XML code
<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="THISISA3.xsl"?>
<forecast qTime="28/10/20 10:00 PM" qLocation="Singapore">
<weather yyyymmdd="20200430">
<year>2020</year>
<month>04</month>
<date>30</date>
<comment>Plenty of sunshine</comment>
<code>sunny</code>
<highest>32.6</highest>
<lowest>28.4</lowest>
</weather>
<weather yyyymmdd="20200421">
<year>2020</year>
<month>04</month>
<date>21</date>
<comment>Plenty of sunshine</comment>
<code>sunny</code>
<highest>32.2</highest>
<lowest>29.8</lowest>
</weather>
</forecast>
It is good to provide the required output and also to show the efforts made to get the result as the community here to help with specific problems and not to write the whole code.
However, if you're newbie to start with xslt then below code with comments could help you to get started and to understand the use of stuff in xslt.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output method="xml" indent="yes"/>
<xsl:strip-space elements="*"/>
<!-- key is used to identify nodes within an expression -->
<xsl:key name="highTemp" match="highest" use="."/>
<!-- identity template to result the transformation in source XML doc itself -->
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<!-- matching a root node -->
<xsl:template match="forecast">
<!-- iterating through each 'weather' element -->
<xsl:for-each select="weather">
<!-- selecting node which matches the variable having required highest temperature -->
<xsl:apply-templates select=".[highest=$monthApril]"/>
</xsl:for-each>
</xsl:template>
<xsl:variable name="monthApril">
<!-- iterate only 'weather' elements which are from month of April -->
<xsl:for-each select="//weather[substring(#yyyymmdd,5,2) = '04']">
<!-- sorting in descending order so that node having highest temp comes first -->
<xsl:sort select="key('highTemp', highest)" order="descending"/>
<xsl:if test="position()=1">
<xsl:value-of select="highest"/>
</xsl:if>
</xsl:for-each>
</xsl:variable>
</xsl:stylesheet>
It would be nice if you specified what the required output is and how the variables are set and which version of xslt you use.
Below is an example XSLT 1.0 template (it has name 'highestTempForMonth') with the ability to specify the required month (it can be specified in a parameter 'month' for template) and with the ability to call this template with required month in any place you want to do it.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0" >
<xsl:output method="xml" indent="yes" encoding="Utf-8"/>
<xsl:variable name="requiredMonth" select="'04'"/>
<xsl:template match="/">
<HighestTemperature>
<value>
<xsl:call-template name="highestTempForMonth">
<xsl:with-param name="month" select="$requiredMonth"/>
</xsl:call-template>
</value>
</HighestTemperature>
</xsl:template>
<xsl:template name="highestTempForMonth">
<xsl:param name="month"/>
<xsl:for-each select="/forecast/weather[month = $month]/highest">
<xsl:sort select="." data-type="number" order="descending"/>
<xsl:if test="position() = 1"><xsl:value-of select="."/></xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
There is XSLT 2.0 equivalent (with a little bit simpler template part) example.
<?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="#all" version="2.0" >
<xsl:output method="xml" indent="yes" encoding="Utf-8"/>
<xsl:variable name="requiredMonth" select="'04'"/>
<xsl:template match="/">
<HighestTemperature>
<value>
<xsl:call-template name="highestTempForMonth">
<xsl:with-param name="month" select="$requiredMonth"/>
</xsl:call-template>
</value>
</HighestTemperature>
</xsl:template>
<xsl:template name="highestTempForMonth">
<xsl:param name="month"/>
<xsl:value-of select="max(/forecast/weather[month = $month]/highest/xs:double(.))"/>
</xsl:template>
</xsl:stylesheet>
For both examples if there are no values for the specified month, the 'value' tag with an empty value will be specified. This value can be changed by a simple condition if necessary.
I got a question, I try to convert an XML to JSON using XSLT version 1.0.
As far as the name goes I get it right but the value is another story.
<datapost>
<fields>
<field>
<id>emailId</id>
<name>emailName</name>
<values>
<value>info#example.com</value>
</values>
</field>
</fields>
AS IT CURRENTLY IS:
At the moment I get only the "name" correct but the "value" (emailIdName & emailId & info#example.com) is all the values squashed together what I obviously don't want.
{
"emailName":{
"emailIdemailNameinfo#example.com"
}
}
EXPECTED TO BE:
I want to get only the "name" and the "value" in values (info#example.com)
This is the result that I WANT to get:
{
"emailName":{
"info#example.com"
}
}
This is the code I use:
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="no" encoding="UTF-16" omit-xml-declaration="yes"/>
<xsl:template match="/">
<body>{
<xsl:call-template name="fieldsName"></xsl:call-template>
<xsl:text>
</xsl:text>
</body>
</xsl:template>
<xsl:template name="fieldsName">
<xsl:for-each select="//datapost[position()=1]/fields/field">
"
<xsl:value-of select="name"/>" :
<xsl:call-template name="fieldsValue"/>}
</xsl:for-each>
</xsl:template>
<!-- Array Element -->
<xsl:template match="*" mode="ArrayElement">
<xsl:call-template name="fieldsValue"/>
</xsl:template>
<!-- Object Properties -->
<xsl:template name="fieldsValue">
<xsl:variable name="childName" select="//datapost[position()=1]/fields/field/values/value"/>
<xsl:choose>
<xsl:when test="not(*|#*)">"
<xsl:value-of select="."/>"
</xsl:when>
<xsl:when test="count(*[name()=$childName]) > 1">{ "
<xsl:value-of select="$childName"/>" :[
<xsl:apply-templates select="*" mode="ArrayElement"/>] }
</xsl:when>
<xsl:otherwise>{
<xsl:apply-templates select="#*"/>
<xsl:apply-templates select="*"/>
}
</xsl:otherwise>
</xsl:choose>
<xsl:if test="following-sibling::*">,</xsl:if>
</xsl:template>
<!-- Attribute Property -->
<xsl:template match="#*">"
<xsl:value-of select="name"/>" : "
<xsl:value-of select="."/>",
</xsl:template>
</xsl:stylesheet>
The element names in your XML are poorly chosen, and this has probably confused you (it certainly confused me). One would expect "field" to contain a single field, but it actually contains all of them. So change
<xsl:for-each select="//datapost[position()=1]/fields/field">
to
<xsl:for-each select="//datapost[position()=1]/fields/field/*">
or better still,
<xsl:for-each select="datapost/fields/field/*">
since the other parts of the expression are redundant verbiage.
Then you need to look at the template containing the variable
<xsl:variable name="childName" select="//datapost[position()=1]/fields/field/values/value"/>
This is selecting all the values in the document, not just the values of the current element. I'm not entirely sure what you're trying to achieve here, and it doesn't seem to be covered by your test data, but I suspect you're trying to do some kind of grouping of elements that have the same name. For that you need to use grouping facilities: xsl:for-each-group in XSLT 2.0+, Muenchian grouping in XSLT 1.0. You haven't said which XSLT version you're using, but all of this would be an awful lot easier if you used XSLT 2.0+.
I am processing various XML files with XSLT. In one XML I found a wrapped JSON list:
<list><![CDATA[[
{
"title": "Title 1",
"value": "Value 1",
"order": 1
},
{
"title": "Title 2",
"value": "Value 2",
"order": 2
}
]]]>
</list>
My problem is that I need to iterate over the list. For example:
<xsl:variable name="listVar">
<!-- do something with list -->
</xsl:variable>
<xsl:for-each select="$listVar">
<!-- do something with objects in list e.g. -->
<xsl:value-of select="title"/>
<xsl:value-of select="value"/>
</xsl:for-each>
How to do this with XSLT? I use XSLT 3.0 and Saxon engine, version 9.8 HE.
Considered solutions:
1.
Use parse-json function:
But then I cannot iterate over the result because of XPathException: "Required item type of the context item for the child axis is node(); supplied value (.) has item type array(function(*))" or "Maps cannot be atomized". I found that there are
functions that probably I should take into account like map:get, map:entry, but I've failed to use them in my case so far.
2.
Addidiotnal transform before the one mentioned above:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0">
<xsl:output method="xml" encoding="UTF-8" indent="no"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="list">
<list>
<xsl:copy-of select="json-to-xml(.)"/>
</list>
</xsl:template>
</xsl:stylesheet>
And then:
<xsl:variable name="listVar" select="list/array/map"/>
But it does not work - probably due to added namespace
<list>
<array xmlns="http://www.w3.org/2005/xpath-functions">
<map>
...
Your JSON structure when parsed with parse-json gives you on the XSLT/XPath side an array of maps and the most straightforward way to process the individual array items is with the ?* lookup operator, then you can use for-each or even apply-templates:
<?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:math="http://www.w3.org/2005/xpath-functions/math"
exclude-result-prefixes="xs math"
version="3.0">
<xsl:template match="list">
<xsl:apply-templates select="parse-json(.)?*"/>
</xsl:template>
<xsl:template match=".[. instance of map(xs:string, item())]">
<xsl:value-of select="?title, ?value"/>
</xsl:template>
</xsl:stylesheet>
where to access the map values you can again use ?foo as shown above.
As for working with the XML returned by json-to-xml, it returns elements in the XPath function namespace so to select them, as with any other elements in a namespace you need to make sure you set up a namespace with e.g. xpath-default-namespace for the section you want to process elements from that namespace or you can use the namespace wild card e.g. *:array/*:map.