XSL : how to select a unique nodes in a nodeset - xslt

I have been reading on the different question talking about selecting unique nodes in a document (using the Muenchian method) but in my case I cannot use keys (or I do not know how) because I am working on a node set and not on the document.
And keys cannot be set on a node-set. Basically I have a variable:
<xsl:variable name="limitedSet" select="
$deviceInstanceNodeSet[position() <= $tableMaxCol]"
/>
which contains <deviceInstance> nodes which themselves containing <structure> elements
the node set may be represented this way:
<deviceInstance name="Demux TSchannel" deviceIndex="0">
<structure name="DemuxTschannelCaps">
</structure>
</deviceInstance>
<deviceInstance name="Demux TSchannel" deviceIndex="1">
<structure name="DemuxTschannelCaps">
</structure>
</deviceInstance>
<deviceInstance name="Demux TSchannel" deviceIndex="3">
<structure name="otherCaps">
</structure>
</deviceInstance>
And I do not know a to select <structure> elements that only have different name. The select would in this example return two <structure> elements, being:
<structure name="DemuxTschannelCaps"></structure>
<structure name="otherCaps"></structure>
I have tried
select="$limitedSet//structure[not(#name=preceding::structure/#name)]"
but the preceding axis goes all along the document and not the $limitedSet?
I am stuck, can someone help me. Thank you.

<xsl:variable name="structure" select="$limitedSet//structure" />
<xsl:for-each select="$structure">
<xsl:variable name="name" select="#name" />
<xsl:if test="generate-id() = generate-id($structure[#name = $name][1])">
<xsl:copy-of select="." />
</xsl:if>
</xsl:for-each>
This could be aided by a key:
<xsl:key name="kStructureByName" match="structure" use="#name" />
<!-- ... -->
<xsl:if test="generate-id() = generate-id(key('kStructureByName', $name)[1])">
Depending on your input, the key would have to capture some additional contextual information:
<xsl:key name="kStructureByName" match="structure" use="
concat(ancestor::device[1]/#id, ',', #name)
" />
<!-- ... -->
<xsl:variable name="name" select="concat(ancestor::device[1]/#id, ',', #name)" />
<xsl:if test="generate-id() = generate-id(key('kStructureByName', $name)[1])">

select="$limitedSet//structure[not(#name=preceding::structure[count($limitedSet) = count($limitedSet | ..)]/#name)]"

Related

How to insert an element into a identified element in xslt?

I have XML-file with complex hierarchical structure, which I copy with document fileName.xml. I want to insert a new element into other element. The target element calculating based on input file with concat('b_',$id).
For example fileName.xml:
<root>
<transform id="b_0">
<transform id="b_1">
<transform id="b_2">
<transform id="b_3"/>
<transform id="b_4"/>
</transform>
</transform>
</transform>
</root>
This is example of result document:
<root>
<transform id="b_0">
<transform id="obj_1"/>
<transform id="b_1">
<transform id="b_2">
<transform id="b_3">
<transform id="obj_2"/>
</transform>
<transform id="b_4"/>
</transform>
</transform>
</transform>
</root>
The pattern of my xslt code:
<xsl:variable name="transforms" select="document('fileName.xml')"/>
<xsl:variable name="table" select="."/>
<xsl:template match="tr">
<xsl:variable name="param" select="$table//tr/td[2]"/>
<xsl:variable name="id" select="concat('b_',$param)"/>
<xsl:copy-of select="$transforms"/>
<xsl:copy>
<Transform>
<xsl:attribute name="id"><xsl:value-of select="concat('obj_', position())"/></xsl:attribute>
<xsl:apply-templates select="$transforms/transform[#id = $id]"/>
</Transform>
</xsl:copy>
</xsl:template>
First of all, it is difficult to understand what you are trying to achieve, you don't show the input to you transform (although from the tag-names "table", "tr", "td", I'm guessing a html-formatted table).
You seem confused by some xslt-concepts:
the xsl:copy-of-tag copies a nodeset (in your case: the contents of the $transforms-variable) to the output
the xsl:copy-tag copies the current tag (in your case, a tr-tag, since that is what is matched by the template) to the output
Also, please note that:
position() will return the relative position of the current node (your tr) within its parent, maybe that is what you want, maybe not
$transforms/transform[#id=$id] will only match transform elements on the top level of $transforms. Use double slash // if you want to match elements on any level.
Now, for transforming the contents of the $transform-variable as you specify, inserting a new element inside an element you specify, you must create:
an identity-transform copying input to output
unless the tag is the one you are looking for, in which case you insert a new element and then copy
Since the tag you are looking for is dynamic, based on the $id-variable, this must be passed as a parameter. Based on your code, I guess you want to insert a new element inside the element having #id=$id as following-sibling.
You can create such a transform with this:
<xsl:template match="node()" mode="tx">
<!-- parameters controlling behaviour as input -->
<xsl:param name="id" />
<xsl:param name="pos" />
<!-- copies the current node -->
<xsl:copy>
<!-- copies attributes of the current node -->
<xsl:copy-of select="#*" />
<!-- if the next sibling with tag transform has id=$id, insert a new element -->
<xsl:if test="following-sibling::transform[position()=1 and #id=$id]">
<transform>
<xsl:attribute name="id">
<xsl:value-of select="concat('obj_', $pos)" />
</xsl:attribute>
</transform>
</xsl:if>
<!-- call recursively, make sure to include the parameters -->
<xsl:apply-templates select="node()" mode="tx">
<xsl:with-param name="id" select="$id"/>
<xsl:with-param name="pos" select="$pos" />
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
Note that I set a mode on this transform to be able to control when it is being called (I just want it called in specific circumstances, not on all input).
This can be called from your transform matching tr like this:
<xsl:template match="tr">
<xsl:variable name="param" select="$table//tr/td[2]" />
<xsl:variable name="id" select="concat('b_',$param)" />
<xsl:apply-templates select="$transforms" mode="tx">
<xsl:with-param name="id" select="$id"/>
<xsl:with-param name="pos" select="position()" />
</xsl:apply-templates>
</xsl:template>
A fully working example available here: http://xsltransform.net/bwdwsA/1

XSLT grouping with filter

I am trying to do XSLT grouping within a call template while applying a filter. The below code works fine but it is not considering the filter condition and is returning all the nodes . Please help me to understand what I am doing wrong . My understanding is that is that it has something to do with the scope of the below line
<xsl:for-each select="key('ParentName',#ParentName)" >
Here is the xml and xslt .
<?xml version="1.0" encoding="utf-16"?>
<Documentation>
<Consequences>
<Nodes>
<Node1 name ="abc1" ParentName="Group1" IsInternal ="true" />
<Node1 name ="abc2" ParentName="Group2" IsInternal ="true"/>
<Node1 name ="bcd1" ParentName="Group2" IsInternal ="true" />
<Node1 name ="bcd2" ParentName="Group1" IsInternal ="false"/>
<Node1 name ="efg1" ParentName="Group2" IsInternal ="false"/>
<Node1 name ="efg2" ParentName="Group1" IsInternal ="false" />
</Nodes>
</Consequences>
</Documentation>
XSLT : -
<xsl:output method="xml" indent="yes"/>
<xsl:template match="Consequences" >
<xsl:call-template name="Template1">
<xsl:with-param name="NodeList" select="Nodes/Node1[#IsInternal='true']"/>
</xsl:call-template>
</xsl:template>
<xsl:key name="ParentName" match="Node1" use="#ParentName" />
<xsl:template name ="Template1">
<xsl:param name="NodeList" />
<xsl:for-each select="$NodeList[generate-id()=generate-id(key('ParentName',#ParentName)[1])]">
<Group>
<xsl:value-of select="#ParentName" />
<xsl:for-each select="key('ParentName',#ParentName)" >
<SubItems>
<xsl:value-of select="#name" />
</SubItems>
</xsl:for-each>
</Group>
</xsl:for-each>
</xsl:template>
Assuming that you are trying to select only the elements who's #IsInternal='true', instead of simply all of the Node1 elements that have the specified #ParenName.
Your xsl:key is matching on all of the Node1 elements and indexed by the #ParentName value. If you are only using that key to retrieve the elements who's #IsInternal='true', then you can add a predicate filter to the match criteria:
<xsl:key name="ParentName" match="Node1[#IsInternal='true']" use="#ParentName" />
If you need to use that key to lookup Node1 elements by #ParentName in other areas without regard to what the #IsInternal value is, then you could filter the items returned from the key lookup:
<xsl:for-each select="key('ParentName',#ParentName)[#IsInternal='true']" >
<SubItems>
<xsl:value-of select="#name" />
</SubItems>
</xsl:for-each>
You could also define a key that uses the value of both the #ParentName and the #IsInternal values as the key value:
<xsl:key name="ParentName" match="Node1"
use="concat(#ParentName,'-',#IsInternal)" />
and then retrieve them with the same key value:
key('ParentName',concat(#ParentName,'-',#IsInternal))

in xslt how to compare a string value with another variable containing multiple values

I have the following xml file. I need to fetch all unique "owner" values from this and perform some operations.
<issues>
<issue>
<owner>12345</owner>
</issue>
<issue>
<owner>87654</owner>
</issue>
<issue>
<owner>12345</owner>
</issue>
</issues>
<tests>
<test>
<owner>34598</owner>
</test>
<test>
<owner>12345</owner>
</test>
<test>
<owner>34598</owner>
</test>
<test>
<owner>11111</owner>
</test>
</tests>
I tried using following xslt script.
<xsl:for-each select="issues/issue[not(child::owner=preceding- sibling::issue/owner)]/owner">
<!--some code-->
</xsl:for-each>
<xsl:for-each select="tests/test[not(child::owner=preceding- sibling::test/owner)]/owner">
<xsl:variable name="IrmAs">
<xsl:value-of select="." />
</xsl:variable>
<xsl:variable name="IssueList">
<xsl:value-of select="//issues/issue/owner">
</xsl:variable>
<xsl:if test="not(contains($IssueList,$IrmAs))">
<!--some code-->
</xsl:if>
</xsl:for-each>
But am getting duplicate values. Could anyone please help me with this?
In XSLT 2.0 you can just use for-each-group:
<xsl:for-each-group select="issues/issue | tests/test" group-by="owner">
<!-- in here, . is the first issue/test with a given owner and current-group()
is the sequence of all issue/test elements that share the same owner -->
</xsl:for-each-group>
If you are stuck on 1.0 then you need to use a technique called "Muenchian grouping" - define a key that groups elements with the same owner, then process just the first item in each group using a generate-id trick
<xsl:key name="ownerKey" match="issue | test" use="owner" />
<xsl:for-each select="(issues/issue | tests/test)[generate-id()
= generate-id(key('ownerKey', owner)[1])]">
<!-- one iteration per unique owner, with . being the parent element of the
first occurrence -->
</xsl:for-each>
But am getting duplicate values.
It's not quite clear where you are getting the duplicate values - since your posted code does not output anything. If you had tested something like:
...
<xsl:for-each select="issues/issue[not(child::owner=preceding-sibling::issue/owner)]/owner">
<out>
<xsl:value-of select="." />
</out>
</xsl:for-each>
....
you would have seen that it does work (albeit inefficiently) and returns:
...
<out>12345</out>
<out>87654</out>
...
Similarly, testing the following snippet:
...
<xsl:for-each select="tests/test[not(child::owner=preceding-sibling::test/owner)]/owner">
<xsl:variable name="IrmAs">
<xsl:value-of select="." />
</xsl:variable>
<xsl:variable name="IssueList">
<xsl:value-of select="//issues/issue/owner"/>
</xsl:variable>
<xsl:if test="not(contains($IssueList,$IrmAs))">
<out>
<xsl:value-of select="." />
</out>
</xsl:if>
</xsl:for-each>
...
produces:
...
<out>34598</out>
<out>11111</out>
...
So the problem must be in the part of the code that you haven't posted. Note also that the code you did post has several syntax errors, e.g. :
<xsl:value-of select="//issues/issue/owner">
needs to be:
<xsl:value-of select="//issues/issue/owner"/>

XSLT: merge two tags using attribute from one, tag from another with a transform/mapping

I have two tags in the input file, variable and type:
<variable baseType="int" name="X">
</variable>
<type baseType="structure" name="Y">
<variableInstance name="X" />
</type>
And I need to generate the following output file:
<Item name="Y">
<Field name="X" type="Long" />
</Item>
So conceptually my approach here has been to convert the type tag into the Item tage, the variable instance to the Field. That's working fine:
<xsl:for-each select="type[#baseType='structure']">
<Item>
<xsl:attribute name="name"><xsl:value-of select="#name" /></xsl:attribute>
<xsl:for-each select="variableInstance">
<Field>
<xsl:attribute name="name"><xsl:value-of select="#name" /></xsl:attribute>
<xsl:attribute name="type">**THIS IS WHERE I'M STUCK**</xsl:attribute>
</Field>
</xsl:for-each>
</Item>
</xsl:for-each>
The problem I'm stuck on is:
I don't know how to get the variableInstance/Field tag to match on the variable tag by name, so I can access the baseType.
I need to map "int" to "Long" once I'm able to do 1.
Thanks in advance!
PROBLEM 1.
For the first problem that you have you can use a key:
<xsl:key name="variable-key" match="//variable" use="#name" />
That key is going to index all variable elements in the document, using their name. So now, we can access any of those elements by using the following XPath expression:
key('variable-key', 'X')
Using this approach is efficient when you have a lot of variable elements.
NOTE: this approach is not valid if each variable has its own scope (i.e. you have local variables which are not visible in different parts of the document). In that case this approach should be modified.
PROBLEM 2.
For mapping attributes you could use a template like the following:
<xsl:template match="#baseType[. = 'int']">
<xsl:attribute name="baseType">
<xsl:value-of select="'Long'" />
</xsl:attribute>
</xsl:template>
The meaning of this transformation is: each time that we match a baseType attribute with int value, it has to be replaced by a Long value.
This transformation would be in place for each #baseType attribute in the document.
Using the described strategies a solution could be:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" />
<!-- Index all variable elements in the document by name -->
<xsl:key name="variable-key"
match="//variable"
use="#name" />
<!-- Just for demo -->
<xsl:template match="text()" />
<!-- Identity template: copy attributes by default -->
<xsl:template match="#*">
<xsl:copy>
<xsl:value-of select="." />
</xsl:copy>
</xsl:template>
<!-- Match the structure type -->
<xsl:template match="type[#baseType='structure']">
<Item>
<xsl:apply-templates select="*|#*" />
</Item>
</xsl:template>
<!-- Match the variable instance -->
<xsl:template match="variableInstance">
<Field>
<!-- Use the key to find the variable with the current name -->
<xsl:apply-templates select="#*|key('variable-key', #name)/#baseType" />
</Field>
</xsl:template>
<!-- Ignore attributes with baseType = 'structure' -->
<xsl:template match="#baseType[. = 'structure']" />
<!-- Change all baseType attributes with long values to an attribute
with the same name but with an int value -->
<xsl:template match="#baseType[. = 'int']">
<xsl:attribute name="baseType">
<xsl:value-of select="'Long'" />
</xsl:attribute>
</xsl:template>
</xsl:stylesheet>
That code is going to transform the following XML document:
<!-- The code element is present just for demo -->
<code>
<variable baseType="int" name="X" />
<type baseType="structure" name="Y">
<variableInstance name="X" />
</type>
</code>
into
<Item name="Y">
<Field baseType="Long" name="X"/>
</Item>
Oki I've got a solution for point 1 xsltcake slice or with use of templates. For point two I would probably use similar template to the one that Pablo Pozo used in his answer

XPath expression to count unique descendent elements

I am writing an XPath expression to count unique child attribues. Using the following xPath expression I could fetch all the child attributes along with which are not unique:
//*[count(*)=0]
I need an XPath expression to return me all the unique attributes and the count of number of unique attributes
Eg: XML file
<details>
<Employee>
<EmpNo>10</EmpNo>
<EmpName>TestName</EmpName>
<Address>
<Address1>market</Address1>
<Address2>motel</Address2>
<Street/>
</Address>
</Employee>
<Employee>
<EmpNo>20</EmpNo>
<EmpName>TestName2</EmpName>
<Address>
<Address1>school</Address1>
<Address2>playground</Address2>
<Street>
<StreetName>TestStreet2</StreetName>
<StreetCode>200</StreetCode>
</Street>
</Address>
</Employee>
Expected output:
<!-- Unique element's count -->
<data>6</data>
<!-- Unique Element Names -->
<data>EmpNo</data>
<data>EmpName</data>
<data>Address1</data>
<data>Address2</data>
<data>StreetName</data>
<data>StreetCode</data>
<!-- Unique Element values -->
<!-- Data Set 1 -->
<data>10</data>
<data>TestName</data>
<data>market</data>
<data>motel</data>
<data>null</data>
<data>null</data>
<!-- Data Set 2 -->
<data>20</data>
<data>TestName2</data>
<data>school</data>
<data>playground</data>
<data>TestStreet2</data>
<data>200</data>
Thanks.
This XSLT 1.0 stylesheet
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output indent="yes" omit-xml-declaration="yes" />
<!-- index data fields by their element name -->
<xsl:key
name = "kFields"
match = "Employee//*"
use = "name()"
/>
<!-- store a unique list of elements (Muenchian Grouping) -->
<xsl:variable name="fields" select="
/details/Employee//*[
generate-id()
=
generate-id(key('kFields', name())[1])
][
not(
key('kFields', name())/*
)
]
" />
<!-- main output ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
<xsl:template match="/details">
<xsl:comment> unique element count </xsl:comment>
<data>
<xsl:value-of select="count($fields)" />
</data>
<xsl:call-template name="newline" />
<xsl:comment> unique element names </xsl:comment>
<xsl:for-each select="$fields">
<data>
<xsl:value-of select="name()" />
</data>
<xsl:call-template name="newline" />
</xsl:for-each>
<xsl:comment> unique element values </xsl:comment>
<xsl:apply-templates select="Employee" />
</xsl:template>
<!-- Employee output ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
<xsl:template match="Employee">
<xsl:variable name="this" select="." />
<xsl:comment> data set <xsl:value-of select="position()" /> </xsl:comment>
<xsl:for-each select="$fields">
<xsl:variable
name="val"
select="$this//*[not(*) and name() = name(current())]"
/>
<data>
<xsl:choose>
<xsl:when test="normalize-space($val) != ''">
<xsl:value-of select="$val" />
</xsl:when>
<xsl:otherwise>
<xsl:text>null</xsl:text>
</xsl:otherwise>
</xsl:choose>
</data>
<xsl:call-template name="newline" />
</xsl:for-each>
</xsl:template>
<!-- Helpers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
<xsl:template name="newline">
<xsl:value-of select="'
'" />
</xsl:template>
</xsl:stylesheet>
produces (line-breaks may reproduce differently for you):
<!-- unique element count -->
<data>6</data>
<!-- unique element names -->
<data>EmpNo</data>
<data>EmpName</data>
<data>Address1</data>
<data>Address2</data>
<data>StreetName</data>
<data>StreetCode</data>
<!-- unique element values -->
<!-- data set 1 -->
<data>10</data>
<data>TestName</data>
<data>market</data>
<data>motel</data>
<data>null</data>
<data>null</data>
<!-- data set 2 -->
<data>20</data>
<data>TestName2</data>
<data>school</data>
<data>playground</data>
<data>TestStreet2</data>
<data>200</data>
Notes:
//*[count(*)=0] and //*[not(*)] are equal. The latter is nicer.
I've used an <xsl:key> and Muenchian Grouping to figure out the unique element names among the descendants of <Employee>
The XPath expression in the variable $fields, does two things:
First it uses Muenchian grouping of the elements to make them unique by name().
Then it checks the remaining elements. No element of the same name may have any children anywhere in the input (not( key('kFields', name())/* ). Otherwise <data>Street</data> would show up in the output.
Your output format is ambiguous. If there are elements that have equal names but different nesting positions, things will get messed up.