How to declare and iterate an array in XSLT? - arrays

My requirement is -using XSLT- to show a dropdown list with the US states and print 'selected' on one specific that is declared in the XML which will use my style sheet.
I was thinking on declare an array with the states and iterate it but I don't know how to do it.
NOTE: More ideas are welcome ;)

One way to do this is to embed the state data into the stylesheet itself, and access the stylesheet document using document(''), as follows:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="whatever"
exclude-result-prefixes="my">
<xsl:output indent="yes"/>
<!-- The value of the state you want to select, supplied in the input XML -->
<xsl:variable name="selected-state" select="/xpath/to/state/value"/>
<!-- You have to use a namespace, or the XSLT processor will complain -->
<my:states>
<option>Alabama</option>
<option>Alaska</option>
<!-- ... -->
<option>Wisconsin</option>
<option>Wyoming</option>
</my:states>
<xsl:template match="/">
<!-- rest of HTML -->
<select name="state">
<!-- Access the embedded document as an internal "config" file -->
<xsl:apply-templates select="document('')/*/my:states/option"/>
</select>
<!-- rest of HTML -->
</xsl:template>
<!-- Copy each option -->
<xsl:template match="option">
<xsl:copy>
<!-- Add selected="selected" if this is the one -->
<xsl:if test=". = $selected-state">
<xsl:attribute name="selected">selected</xsl:attribute>
</xsl:if>
<xsl:value-of select="."/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Let me know if you have any questions.

Ideally you would store the list of states in your XML file and just use XSLT to iterate them.
Update:
If you can't edit the XML, you could look at using the document function to load data from a second data file:

Related

Representing collections data correctly in JSON after XSLT conversion from XML

I have the following XSLT, which is converting XML data to JSON.
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">{
<xsl:apply-templates select="*"/>}
</xsl:template>
<!-- Object or Element Property-->
<xsl:template match="*">
"<xsl:value-of select="name()"/>" : <xsl:call-template name="Properties"/>
</xsl:template>
<!-- Array Element -->
<xsl:template match="*" mode="ArrayElement">
<xsl:call-template name="Properties"/>
</xsl:template>
<!-- Object Properties -->
<xsl:template name="Properties">
<xsl:variable name="childName" select="name(*[1])"/>
<xsl:choose>
<xsl:when test="not(*|#*)">"<xsl:call-template name="ctlchar">
<xsl:with-param name="pText" select="." />
</xsl:call-template>"</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:if test="not(starts-with(name(), 'xsi:'))">
"<xsl:value-of select="name()"/>" : "<xsl:value-of select="."/>",
</xsl:if>
</xsl:template>
<!-- replace carriage return with \n -->
<xsl:template name="ctlchar">
<xsl:param name="pText" select="." />
<xsl:choose>
<xsl:when test="not(contains($pText, '
'))">
<xsl:copy-of select="$pText" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of
select="substring-before($pText, '
')" />
<xsl:text>\n</xsl:text>
<xsl:call-template name="ctlchar">
<xsl:with-param name="pText"
select="substring-after($pText, '
')" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
This works fairly well, but I have run into an issue.
This xml:
<items>
<item></item>
<item></item>
<item></item>
<item></item>
<items>
gets converted to this JSON:
"items" : {
"item" : [
{},
{},
{},
{}
]
}
The items are all contained in an array, and this seems correct. However, the following XML:
<items>
<item></item>
<items>
Gets converted into this:
"items" : {
"item" : {}
}
This makes the JSON difficult to parse, since item is sometimes an array and sometimes an object.
This is an inherited project, I don't know how it started. I'm just trying to figure out if this is conventional? It seems like it should be an array of 1 if it is an array of 4.
I see the same XSLT posted around online as a solution for xml->json so I am confused. Not sure how to tell a client to parse this. I also don't know how to fix the xslt, since it seems like the xslt can't know whether or not a single-item collection should be an array or a single object.
I also don't know how to fix the xslt, since it seems like the xslt can't know whether or not a single-item collection should be an array or a single object.
That's the whole point here. Your XSLT must somehow know if you're dealing with a collection or a single item.
This is quite difficult to solve in general, but rather easy to solve for a specific case.
The condition in <xsl:when test="count(*[name()=$childName]) > 1"> checks if you'll need an array or a single item. You may consider to modify this condition to work correctly for your specific case. That would be a bit of hardcoding, but it's probably the easiest option.
There is no perfect way of converting XML to JSON, and every converter does it differently. The issue you have hit is one of the classic difficulties. If a book has a single author, there is no way of knowing whether other books might have multiple authors and therefore whether the author should be a single item or an array of length 1.
In my view the only way to do the conversion is with semantic knowledge. Both the XML and the JSON representation need to be derived from the underlying semantic data model, not from each other, and the conversion needs to be aware of the data model, which means that purely mechanical conversion is always going to produce unhappy results.
To add to #MichaelKay's answer...
If the XML document was validated against an XSD using a Java XML parser then it should be possible in principle to get the Post-Schema Validation Information (PSVI) for the element and get its xsd:maxOccurs value. Then, if maxOccurs = 1 you could output a single JSON field, else output an array.
However, I don't know of any tool that does this. Not sure why, but I suspect that it is not always possible to know for sure (from the PSVI) which XSD object a particular tag was matched against. Michael may be able to confirm/deny that theory.

Transformation From One XML to Another XML Format Using JAXB in Apache Camel Route without using XSLT

I have request xml which is consumed by target system. Target system accpets XML but format is different.So I need to build marshal and unmarshal logic to get data flowing in my route to Target System. So is there any way where I can achive using Java bean approach or jAXB without using dozer or XSLT
Apache Camel with Springboot
Here is a very quick and simple way to do an XSLT transformation.
Assuming your source data looks like this:
<?xml version="1.0"?>
<root>
<elem1 id="1">
<child1>Hello</child1>
<child2>World</child2>
</elem1>
</root>
you can have a transform XSLT file that looks a bit like:
<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output method="xml" indent="yes" />
<xsl:template match="elem1">
<div>
<h1>Section <xsl:value-of select="#id" /></h1>
<p><xsl:value-of select="child1" /></p>
<p><xsl:value-of select="child2" /></p>
</div>
</xsl:template>
<xsl:template match="#*|node()"> <!-- identity transform -->
<xsl:copy>
<xsl:apply-templates select="#*"/>
<xsl:apply-templates select="node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
so you then pass this via the camel xslt transform, one way or another, for example
from("direct:start")
.to("xslt:myTransformFile.xsl&saxon=true")
.to("direct:end")
you should then end up with the following XML message being passed to direct:end:
<?xml version="1.0"?>
<root>
<div>
<h1>Section 1</h1>
<p>Hello</p>
<p>World</p>
</div>
</root>
because the identity template has copied any elements that are not matched by another template, and the template replaces any element that match the selection criteria, with the contents of the template. In this case elem1 is replaced, by the contents of the template. The <xsl:value-of ... /> elements are interpreted but any element that doesn't begin <xsl:... is just part of the output.
For a simple introduction to XSLT, you can look here: https://www.w3schools.com/xml/xsl_intro.asp.

XSLT-1.0 array disable sorting

I have many similar operations, for example, check that one tag is sum of two other tags.
To write it once, I do next:
<xsl:variable name="psumArray" select="//AAA/SUM1 | //AAA/SUM2"/>
<xsl:variable name="psummandArray1" select="//AAA/A1 | //AAA/A2"/>
<xsl:variable name="psummandArray2" select="//AAA/B1 | //AAA/B2"/>
<xsl:for-each select="$psumArray">
<xsl:variable name="temppos" select="position()"></xsl:variable>
<xsl:if test=" format-number((text()), '#.##') != format-number(number($psummandArray1[$temppos]/text()) + number($psummandArray2[$temppos]/text())), '#.##')">
<ERROR>error!</ERROR>
</xsl:if>
</xsl:for-each>
And I can put to my array variables any number of "selects". But I found that each array sorts it's result by name. And order of members in array is not defined by sequence that I wrote, but defined by select text comparation.
I.e.:
<xsl:variable name="psummandArray2" select="//AAA/B3 | //AAA/B2"/>
Will become as:
<xsl:variable name="psummandArray2" select="//AAA/B2 | //AAA/B3"/>
After sorting.
How to avoid it? Or how to achieve what I want to another way, if that way is not the best?
UPD:
Imput XML is similar to:
<XML>
<A1>50</A1>
<A2>20</A2>
<A3>70</A3>
<A4>90</A4>
<A5>5</A5>
<A6>45</A6>
<A7>35</A7>
<A8>25</A8>
<A9>80</A9>
<A10>110</A10>
<A11>100</A11>
<A12>30</A12>
<A13>70</A13>
...
<A120>33</A120>
</XML>
And there exists rules, such as:
A3 = A6 + A8
A13 = A1 + A2
and etc. More then hundred rules. I have to write XSLT that would check all those rules, and if rule is wrong at some nodes, I have to print <ERROR>Error!</ERROR>
The best I think is to write XSLT, where I need just to add name of sum node and names of summand nodes.
That is why I wrote xslt as above, to check it. And now I was going just to put all rules to arrays. And then I found it sorts arrays, breaking my plans :-)
I have ~100 tasks to check if one node is a sum of two another nodes.
And I don't want to write comparation 100 times
Well, you have to write it out at least once, since it doesn't seem to follow any kind of logic that could be automated. Here's one way you could do it:
<?xml version="1.0" encoding="UTF-8"?>
<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:template match="/XML">
<xsl:variable name="tests">
<xsl:call-template name="test-sum">
<xsl:with-param name="sum" select="A3"/>
<xsl:with-param name="summands" select="A6 | A8"/>
</xsl:call-template>
<xsl:call-template name="test-sum">
<xsl:with-param name="sum" select="A13"/>
<xsl:with-param name="summands" select="A1 | A2"/>
</xsl:call-template>
<!-- ... -->
</xsl:variable>
<xsl:if test="contains($tests, 'error')">
<ERROR>Error!</ERROR>
</xsl:if>
</xsl:template>
<xsl:template name="test-sum">
<xsl:param name="sum"/>
<xsl:param name="summands"/>
<xsl:if test="$sum!=sum($summands)">
<xsl:text>error</xsl:text>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Alternatively, you could express your rules as XML - either in an external document, or in the stylesheet itself, then apply a template to that.
With XSLT 2.0 and an XSLT 2.0 processor you can use e.g. <xsl:variable name="psumArray" select="//AAA/SUM1 , //AAA/SUM2"/> to get a sequence of SUM1 elements followed by SUM2 elements.

Can not loop through FOR XML AUTO results

I am taking some data from DB through stored procedure and i am using FOR XML AUTO to get this data.
query is like this
WHERE ITEMNUMBER=#itemid AND DATASET = #dataset FOR XML AUTO, ELEMENTS) AS filters
and result xml is
<filters extra="filters">
<ISP_WebItem>
<FILTER>Type</FILTER>
<FILTERNAME>Matematik</FILTERNAME>
<UNITCODE></UNITCODE>
</ISP_WebItem>
<ISP_WebItem>
<FILTER>Strømkilde</FILTER>
<FILTERNAME>Solceller</FILTERNAME>
<UNITCODE></UNITCODE>
</ISP_WebItem>
</filters>
Now my problem is that when i try to loop through this result in my xslt,I am unable to get any result.
My XSLT code is like this
<xsl:variable name="filt" select="msxsl:node-set(filters)"/>
<xsl:for-each select="$filt/ISP_WebItem">
<xsl:copy-of select="FILTER"/>
</xsl:for-each>
I am getting the filter xml correctly in my 'filt' variable, but the for each loop never executes correctly.
When i tried putting break points in my code,I noticed that code execution never enters inside the loop ,that means
<xsl:for-each select="filt/ISP_WebItem">
this piece of code never satisfies.
And for extra info this filter node is a part of large XML data,every other nodes in this data is working correctly(ie i can loop or do any operation with them).
can any one suggest any possible reasons.
There are two problems with your XSLT. Firstly, filt is a variable, and so you need to prefix it with a $ to indicate this, otherwise the XSLT will be looking for an element named filt
<xsl:for-each select="$filt/ISP_WebItem">
Secondly, in your XML sample, FILTER is an element, but your xsl:copy-of statement is looking for an attribute named 'FILTER'. You should be doing this
<xsl:copy-of select="FILTER"/>
However, if it is an attribute, leave it as <xsl:copy-of select="#FILTER"/>
This XSLT would work, for example
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" extension-element-prefixes="msxsl">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<xsl:variable name="filt" select="msxsl:node-set(filters)"/>
<xsl:for-each select="$filt/ISP_WebItem">
<xsl:copy-of select="FILTER"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
In fact, if you did just want to copy the elements, and nothing else, you could remove the xsl:for-each and simplify it to this
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" extension-element-prefixes="msxsl">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<xsl:variable name="filt" select="filters"/>
<xsl:copy-of select="$filt/ISP_WebItem/FILTER"/>
</xsl:template>
</xsl:stylesheet>
However, I am not quite sure if you need to use node-set in this instance (although I guess you are just showing an abdriged code sample, so maybe it is necessary for something elsewhere).
The following would also work
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" extension-element-prefixes="msxsl">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<xsl:for-each select="filters/ISP_WebItem">
<xsl:copy-of select="FILTER"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
EDIT: If you are still getting problems, try one of the following expessions instead.
<xsl:for-each select="$filt//ISP_WebItem">

XML databinding to TreeView (or Tab control), bind attribute based on different attribute

I have some xml:
<Test>
<thing location="home" status="good"/>
<thing location="work" status="bad"/>
<thing location="mountains" status="good"/>
</Test>
The leaves on the TreeView are the values of the status attribute; the nodes will be the value of the location attribute.
├──bad
│.....└──work
└──good
.......├──home
.......└──mountains
Currently, I populate the TreeView (or TabControl) manually, iterating through the xml, adding the nodes to the appropriate leaf.
Can this be done via databinding? I'm guessing a Converter will be involved...
Thanks for any advice.
Assuming you are going to bind to an XmlDataSource you could use a TransformFile with the following contents:
<?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="/Test">
<Test>
<good>
<xsl:for-each select="thing[#status='good']">
<xsl:element name="{#location}"/>
</xsl:for-each>
</good>
<bad>
<xsl:for-each select="thing[#status='bad']">
<xsl:element name="{#location}"/>
</xsl:for-each>
</bad>
</Test>
</xsl:template>
</xsl:stylesheet>
Add an XPath="/Test/*" property to the XmlDataSource to remove the "Test" root element.

Resources