XSLT Choose - testing attribute value - xslt

I'm strugling with a Choose statement and the corresponding Test-Clause.
If have the following XML (just an extract) which represents an datamodel exportet from Enterprise Architect as XMI:
<xmi:XMI xmi:version="2.1" xmlns:uml="http://schema.omg.org/spec/UML/2.1" xmlns:xmi="http://schema.omg.org/spec/XMI/2.1" xmlns:thecustomprofile="http://www.sparxsystems.com/profiles/thecustomprofile/1.0" xmlns:EAUML="http://www.sparxsystems.com/profiles/EAUML/1.0"> <xmi:Documentation exporter="Enterprise Architect" exporterVersion="6.5"/> <uml:Model xmi:type="uml:Model" name="EA_Model" visibility="public"> <packagedElement xmi:type="uml:Package" xmi:id="EAPK_F3388CFE_57A7_4d84_8866_3FB3AADE565A" name="Data Model - SQLServer2012" visibility="public">
<packagedElement xmi:type="uml:Artifact" xmi:id="EAID_B62341D4_41C6_4c83_A60A_4CA65C2E185E" name="Database SQLServer2012" visibility="public"/>
<packagedElement xmi:type="uml:Package" xmi:id="EAPK_BA7676C5_40BC_4bd9_A0F5_F6B15E534E8E" name="Logical Model" visibility="public">
<packagedElement xmi:type="uml:Class" xmi:id="EAID_2DC36189_CCFB_40bf_A1CB_CD4FB08FE8B5" name="AnamneseStatus" visibility="public">
<ownedAttribute xmi:type="uml:Property" xmi:id="EAID_9BBF5184_37F8_4729_9DC1_7ED3B4D8FC98" name="RCHIUNET05_ContextKey" visibility="public" isStatic="false" isReadOnly="false" isDerived="false" isOrdered="true" isUnique="false" isDerivedUnion="false">
<lowerValue xmi:type="uml:LiteralInteger" xmi:id="EAID_LI000001_37F8_4729_9DC1_7ED3B4D8FC98" value="1"/>
<upperValue xmi:type="uml:LiteralInteger" xmi:id="EAID_LI000002_37F8_4729_9DC1_7ED3B4D8FC98" value="1"/>
<type xmi:idref="EASQL_Server_2012_nvarchar"/>
</ownedAttribute>
<ownedAttribute xmi:type="uml:Property" xmi:id="EAID_BC1F93D0_A7F4_474c_A27E_26D3ABCCFB7B" name="MRNCmpdId" visibility="public" isStatic="false" isReadOnly="false" isDerived="false" isOrdered="false" isUnique="true" isDerivedUnion="false">
<lowerValue xmi:type="uml:LiteralInteger" xmi:id="EAID_LI000003_A7F4_474c_A27E_26D3ABCCFB7B" value="1"/>
<upperValue xmi:type="uml:LiteralInteger" xmi:id="EAID_LI000004_A7F4_474c_A27E_26D3ABCCFB7B" value="1"/>
<type xmi:idref="EASQL_Server_2012_nvarchar"/>
</ownedAttribute>
..........
So with my XSL I will loop throug the relevant nodes and extract the tablenames and attributes. this works without problem. Now I need to translate the EA datatype into another datatype definition.
Lets say: EASQL_Server_2012_nvarchar needs to become System.String
The XSL doing this looks like this (since there are other datatypes the Choose Statement will be longer than showed here):
<xsl:for-each select="ownedAttribute[#xmi:type='uml:Property']">
<xsl:text disable-output-escaping="yes"><</xsl:text>
<xsl:value-of select="'Element Name="'"/>
<xsl:value-of select="#name"/>
<xsl:value-of select="'" '"/>
<xsl:choose>
<xsl:when test="#xmi:idref = 'EASQL_Server_2012_nvarchar'">
<xsl:value-of select="'Type="'"/>
<xsl:value-of select="'System.String'"/>
<xsl:value-of select="'" '"/>
<xsl:value-of select="'MaxLength="'"/>
<xsl:value-of select="'400'"/>
<xsl:value-of select="'" '"/>
</xsl:when>
<xsl:when test="#xmi:idref = 'EASQL_Server_2012_int'">
<xsl:value-of select="'Type="'"/>
<xsl:value-of select="'System.Int32'"/>
<xsl:value-of select="'" '"/>
</xsl:when>
......
Now my problem is, that it will not hit the test conditions and always run into the "otherwise" statement.
Does somebody see why the test condition is not working?
Thank you for any help on this.
Cheers
Sandro

I suspect that instead of:
<xsl:when test="#xmi:idref = 'EASQL_Server_2012_nvarchar'">
you want to do:
<xsl:when test="type/#xmi:idref = 'EASQL_Server_2012_nvarchar'">
since in the partial example you have posted, ownedAttribute (which is your context node when you run this test) has no xmi:idref attribute, but its child element type does.
P.S. I don't know what you're doing overall, but I cringe whenever I see:
<xsl:text disable-output-escaping="yes"><</xsl:text>
This should never be necessary. If - as it seems - you're not outputting XML, set the output method to text. Then it's not necessary to disable output escaping.

Related

Duplicates in a map

I currently have an XSLT function that loads key=value pairs from a text file into a map.
<xsl:function name="myns:loadMapping" as="map(*)">
<xsl:variable name="mapping" as="map(xs:string, xs:string)">
<xsl:map>
<xsl:for-each select="unparsed-text-lines($inputFile,$fileEncoding)">
<!-- Takes only lines which are in the form abc=xyz and are not comments (does not start with #) -->
<xsl:if test="contains(.,'=') and not(starts-with(.,'#'))">
<xsl:map-entry key="substring-before(.,'=')" select="substring-after(.,'=')"/>
</xsl:if>
</xsl:for-each>
</xsl:map>
</xsl:variable>
<xsl:sequence select="$mapping"/>
</xsl:function>
The function works fine unless the user tries to load a file containing duplicates, in which case the XSLT transform fails with an error (expected behaviour):
Error evaluating (map:merge(...)) on line xyz column xy of xyz.xsl:
XTDE3365: Duplicate key in constructed map: {keyInError}
Is there a way I could catch this case and keep the transformation from aborting, something like this :
<xsl:function name="myns:loadMapping" as="map(*)">
<xsl:variable name="mapping" as="map(xs:string, xs:string)">
<xsl:map>
<xsl:for-each select="unparsed-text-lines($inputFile,$fileEncoding)">
<!-- Takes only lines which are in the form abc=xyz and are not comments (does not start with #) -->
<xsl:if test="contains(.,'=') and not(starts-with(.,'#'))">
<xsl:choose>
<xsl:when test="...map contains key...">
<xsl:message>Map already contains key. Please check input file.</xsl:message>
</xsl:when>
<xsl:otherwise>
<xsl:map-entry key="substring-before(.,'=')" select="substring-after(.,'=')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:for-each>
</xsl:map>
</xsl:variable>
<xsl:sequence select="$mapping"/>
</xsl:function>
I see that there is something implemented for a future XSLT 4.0 release (Saxon - Controlling duplicates on xsl:map) but I would like to stick to XSLT 3.0 for the time being.
Thanks.
To add to Martin Honnen's suggestions, you could use xsl:iterate instead of xsl:for-each, passing the map as a parameter, which would allow you to inspect the map before adding another entry to it.
<xsl:iterate select="...">
<xsl:param name="map" select="map{}"/>
<xsl:choose>
<xsl:when test="map:contains($map, ...)">...</xsl:when>
<xsl:otherwise>
<xsl:next-iteration>
<xsl:with-param name="map" select="map:put($map, ..., ...)"/>
Well, both map:merge in XPath 3.1 or of course grouping with e.g.
<xsl:for-each-group select="unparsed-text-lines($inputFile,$fileEncoding)[contains(.,'=') and not(starts-with(.,'#'))]" group-by="substring-before(., '=')">
<xsl:map-entry key="current-grouping-key()" select="substring-after(., '=')"/>
<xsl:if test="current-group()[2]">
<xsl:message>..</xsl:message>
</xsl:if>
</xsl:for-each-group>
allow you more control than your approach without having to wait for XSLT 4 or trying to use experimental extensions.

Populate a variable with a subtree

in a version="2.0" stylesheet:
the following code produces the correct output
<xsl:variable name="obj">
<xsl:choose>
<xsl:when test="t:ReferencedObjectType='Asset'">
<xsl:value-of select="/t:Flow/t:FHeader/t:Producer/t:Repository" />
</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:choose>
<xsl:value-of select="$obj"/>
but this one does not
<xsl:variable name="obj">
<xsl:choose>
<xsl:when test="t:ReferencedObjectType='Asset'">
<xsl:value-of select="/t:Flow/t:FHeader/t:Producer" />
</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:choose>
<xsl:value-of select="$obj/t:Repository"/>
How can I get the second code to run as expected ?
If needed, is there a solution in v3 ?
this code does not run either
<xsl:variable name="obj">
<xsl:choose>
<xsl:when test="t:ReferencedObjectType='Asset'">
<xsl:copy-of select="/t:Flow/t:FHeader/t:Producer" />
</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:choose>
<xsl:value-of select="$obj/t:Repository"/>
relevant xml input
<Flow>
<FHeader>
<Producer>
<Repository>tests.com</Repository>
</Producer>
</FHeader>
</Flow>
You can simply select <xsl:variable name="obj" select="/t:Flow/t:FHeader/t:Producer/t:Repository[current()/t:ReferencedObjectType='Asset']"/>. Or, as Tim already commented, use xsl:copy-of, also taking into account that you then later on need e.g. $obj/t:Producer/t:Repository to select the right level.
Or learn about the as attribute and use e.g. <xsl:variable name="obj" as="element()*">...<xsl:copy-of select="/t:Flow/t:FHeader/t:Producer"/> ...</xsl:variable>, then you later on can use e.g. $obj/t:Repository.
There is also xsl:sequence to select input nodes instead of copying them, in particular with xsl:variable if you use the as attribute. This might consume less memory.
Furthermore XPath 2 and later have if (condition-expression) then expression else expression conditional expressions at the expression level so you might not need XSLT with xsl:choose/xsl:when but could use the <xsl:variable name="obj" select="if (t:ReferencedObjectType='Asset']) then /t:Flow/t:FHeader/t:Producer else if (...) then ... else ()"/>, that way you would select e.g. an input t:Producer element anyway and if you use the variable you can directly select the t:Repository child.

Unable to cast from XRTreeFrag into XNodeSet

I have the following test code... I am trying to pass a node-set as a param. After many hours, i finally was able to pass it to my template.
How I pass my node-set to the template:
<xsl:call-template name="listing">
<xsl:with-param name="customData">
<xsl:apply-templates select="exslt:node-set($data)"/>
</xsl:with-param>
</xsl:call-template>
How my template receives it:
<xsl:template name="listing">
<xsl:param name="customData" select="/.."/>
<xsl:variable name="data">
<xsl:choose>
<xsl:when test="not($customData)">
<xsl:value-of select="/data"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$customData"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<textarea><xsl:copy-of select="$data"></xsl:copy-of></textarea>
</xsl:call-template>
If I set the parameters with a one liner, then it would not complain... example:
<xsl:variable name="data" select="$customData"/>
But as soon as I try to set it like this, it breaks:
<xsl:variable name="data">
<xsl:value-of select="$customData"/>
</xsl:variable>
Getting this error message:
org.apache.xpath.objects.XRTreeFrag cannot be cast to org.apache.xpath.objects.XNodeSet
I was only been able to find another thread dated back in 2000, talk about this similar issue... I need to re-nodeset it back using something like node-set($customData)/* but I tried that, and it was a no go.
EDIT:
OK, I can confirm that I successfully passed the node-set inside my template. But I'm still unable to copy it over to my variable... It kept saying that it is still a RTF.
<xsl:template name="listing">
<xsl:param name="customData" as="node-set"/>
<!--<xsl:variable name="data" select="/data"/>-->
<xsl:variable name="data">
<xsl:choose>
<xsl:when test="count($customData) != 0">
<xsl:copy-of select="$customData"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="/data"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<textarea><xsl:value-of select="$customData/record[1]"/></textarea>
<textarea><xsl:value-of select="/data/record[1]"/></textarea>
<textarea><xsl:value-of select="$data/record[1]"/></textarea>
</xsl:template>
The above test, shows that I can access $customData and the original /data without any problem, they both show the record... but $data is messed up. So that means the copy from $customData to $data wasn't working...
I tried the following ways, none of them work:
<xsl:copy-of select="$customData"/>
<xsl:value-of select="$customData"/>
<xsl:apply-templates select="exslt:node-set($customData)"/>
<xsl:apply-templates select="exslt:node-set($customData)/data"/>
Any idea...?
This error message comes from Xalan, which is an XSLT 1.0 processor. If you are using Xalan, then you are probably using Java, which means there is really no reason at all not to move to XSLT 2.0 in the form of Saxon. You will find that XSLT 2.0 removes many of the restrictions of XSLT 1.0, of which this is one of the most irritating.
If there's a good reason why you can't move forward to XSLT 2.0 (and it's hard to think of one), there's a workaround in the form of the exslt:node-set() function, which converts a result-tree fragment (that is, a variable defined using child instructions) into a document node.
Got it working, basically rather than using apply-template, i need to pass the RTF as a parameter to the template. That is the only way I got it to work.
<xsl:with-param name="data" select="exslt:node-set($customData)"/>
Using this method, I was able to MODIFY data in XSL level. This is really cool, I basically manipulate the data I want, then i reconstruct the root /, and then I pass my customData to my template function.
So rather than reading data off the root, I read my own modified data (constructed inside XSL).
Use of exslt:node-set does indeed suppress the error message org.apache.xpath.objects.XRTreeFrag cannot be cast to org.apache.xpath.objects.XNodeSet
However, the node-set that is created for some reason cannot be used in subsequent XPath expressions; at least it doesn't seem to work with Xalan 2.6.0 / XSLT 1.0 which is the version many people are forced to use for one reason or another.
There is a simple solution: instead of setting the variable to a node-set, set it to the XPath expression instead. Then you can use the dyn:evaluate EXSLT function to evaluate the XPath expression held by the variable.
Your code would look something like this:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:dyn="http://exslt.org/dynamic"
extension-element-prefixes="dyn"
exclude-result-prefixes="dyn">
..
<xsl:variable name="data">
<xsl:choose>
<xsl:when test="count(.) != 0">
<xsl:text>.</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>/data</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<textarea>
<xsl:value-of select="dyn:evaluate($data)/record[1]"/>
</textarea>

XSLT: contains() for multiple strings

I have a variable in XSLT called variable_name which I am trying to set to 1, if the Product in question has attributes with name A or B or both A & B.
<xsl:variable name="variable_name">
<xsl:for-each select="product/attributes">
<xsl:if test="#attributename='A' or #attributename='B'">
<xsl:value-of select="1"/>
</xsl:if>
</xsl:for-each>
</xsl:variable>
Is there any way to match multiple strings using the if statement, as mine just matches if A is present or B is present. If both A & B are present, it does not set the variable to 1. Any help on this would be appreciated as I am a newbie in XSLT.
You can use xsl:choose statement, it's something like switch in common programming languages:
Example:
<xsl:variable name="variable_name">
<xsl:for-each select="product/attributes">
<xsl:choose>
<xsl:when test="#attributename='A'">
1
</xsl:when>
<xsl:when test=" #attributename='B'">
1
</xsl:when>
<!--... add other options here-->
<xsl:otherwise>1</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:variable>
This will set new variable with name variable_name with the value of attribute product/attributes.
For more info ... http://www.w3schools.comwww.w3schools.com/xsl/el_choose.asp
EDIT: And another way (a little dirty) by OP's request:
<xsl:variable name="variable_name">
<xsl:for-each select="product/attributes">
<xsl:if test="contains(text(), 'A') or contains(text(), 'B')">
1
</xsl:if>
</xsl:for-each>
</xsl:variable>
It will be helpful if you provide the xml you're writing your xslt against.
This might not help...
Is it 'legal' to have two XML element attributes with the same name (eg. <element x="1" x="2" />)?
Is this what you are trying to process? Try parsing your XML file through xmllint or something like it to see if it is valid.
xmllint --valid the-xml-file.xml
My guess is that you will get a 'attribute redefined' error.

Different way of sorting a list, depending on a variable in XSLT

I am making a newslist, and have until now sorted the news after their date. Newest first.
But I would like to give the administrator of the site a more flexible solution. This means that in the backend, the admin can choose from a dropdown-list, in wich way he/she want's the list to be sorted. By date(newest first and oldest first), or by title (A-Z and Z-A). This means 4 possible ways right.
Right now I have the following XSLT:
<xsl:variable name="alleNyheder" select="$currentPage//node" />
<xsl:variable name="sort">
<news>
<xsl:for-each select="$alleNyheder[#template='1092']">
<news>
<id>
<xsl:value-of select="./#id"></xsl:value-of>
</id>
<date>
<xsl:choose>
<xsl:when test="./data[#alias='date'] != ''">
<xsl:value-of select="./data[#alias='date']"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="./#createDate"/>
</xsl:otherwise>
</xsl:choose>
</date>
</news>
</xsl:for-each>
</news>
</xsl:variable>
<xsl:for-each select="msxml:node-set($sort)/news/news">
<xsl:sort data-type="text" select="date" order="descending" />
---- My newsitems ----
</xsl:for-each>
So in the moment I sort my list after the "date"-value in my variable $sort.
If I change the "date"-field in $sort, to titles instead of date's I can actually sort my list after the titles of the news. But unfortunately it should be sorted in an ascending order, instead of a descending order. And I can't figure out how to change the order-value dynamically like I do in the select-value.
If it helps anyone, I am working on Umbraco CMS.
Thanks
-Kim
<xsl:choose>
<xsl:when test="$sortfield = 'date' and $sortorder = 'D'>
<xsl:for-each select="msxml:node-set($sort)/news/news">
<xsl:sort data-type="text" select="date" order="descending" />
<!-- ... -->
</xsl:for-each>
</xsl:when>
<xsl:when test="$sortfield = 'date' and $sortorder = 'A'>
<xsl:for-each select="msxml:node-set($sort)/news/news">
<xsl:sort data-type="text" select="date" order="ascending" />
<!-- ... -->
</xsl:for-each>
</xsl:when>
<!-- ... -->
</xsl:choose>
On a different note, you really should look into <xsl:apply-templates> and avoid <xsl:for-each>. Your code gets cleaner and a lot more idiomatic this way. I am also sure that the entire temporary node-set() business is completely avoidable.