copy an xml to another but changing node attribute and edge attributes - xslt

I have an xml with the format
<graph id=1>
<nodes>
<node id =2>
<name value=node1/>
</node>
<node id =3>
<name value=node3/>
</node>
<edges>
<edge id=11 source=2 target=3/>
</edges>
</graph>
Now i want to change the id of node using generate-id() but that should change in all edges too.Eg i change the id of node1 to '1a1' so it should change the source of edge to '1a1' everwhere in xml.
It should do this for all nodes and edges.The remaining xml should be as it is.
My xsl
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="#id[parent::node]">
<xsl:attribute name="id">
<xsl:value-of select="generate-id()"/>
</xsl:attribute>
</xsl:template>
this changes the node id but i want to compare the edges source and target and change them too.
The edge source and target are some nodes id .
Any help would be greatly appreciated.
Thanks

This stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="kElementById" match="*[#id]" use="#id"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="#id">
<xsl:attribute name="id">
<xsl:value-of select="generate-id(..)"/>
</xsl:attribute>
</xsl:template>
<xsl:template match="#source|#target">
<xsl:attribute name="{name()}">
<xsl:value-of select="generate-id(key('kElementById',.))"/>
</xsl:attribute>
</xsl:template>
</xsl:stylesheet>
With this well formed input:
<graph id="1">
<nodes>
<node id ="2">
<name value="node1"/>
</node>
<node id ="3">
<name value="node3"/>
</node>
<edges>
<edge id="11" source="2" target="3"/>
</edges>
</nodes>
</graph>
Output:
<graph id="IDAEQBBB">
<nodes>
<node id="IDAHQBBB">
<name value="node1"></name>
</node>
<node id="IDALQBBB">
<name value="node3"></name>
</node>
<edges>
<edge id="IDAQQBBB" source="IDAHQBBB" target="IDALQBBB"></edge>
</edges>
</nodes>
</graph>

Add this section to the XSL that you already have.
<xsl:template match="#source[parent::edge]|#target[parent::edge]">
<xsl:attribute name="{name()}">
<xsl:value-of select="generate-id(//node[#id=current()]/#id)"/>
</xsl:attribute>
</xsl:template>

Related

Creating taxonomic structure from individual records

I am having a failure of imagination in how to effectively solve this problem. My actual data set has thousands of thousands of records. Each record indicates its location in a taxonomic structure. I need to create that taxonomic structure and place the records within that structure (e.g., records that indicate they go in "/a/b/c" end up in the "/a/b/c" but there is only one each of the taxonomic levels "a", "b", and "c"). Due to client confidentiality, I've posted a naive representation of this here.
Using XSLT 3 is desirable. I know there is a solution to this using xsl:iterate but I cannot figure it out.
Input:
<outer>
<record>
<id>rec1</id>
<taxNodes>
<node>
<id>1</id>
<note>First level</note>
<node>
<id>node2a</id>
<note>Second level Entry A</note>
</node>
</node>
</taxNodes>
</record>
<record>
<id>rec3</id>
<taxNodes>
<node>
<id>1</id>
<note>First level</note>
<node>
<id>node2b</id>
<note>Second level Entry B</note>
</node>
</node>
</taxNodes>
</record>
<record>
<id>rec4</id>
<taxNodes>
<node>
<id>1</id>
<note>First level</note>
<node>
<id>node2b</id>
<note>Second level Entry B</note>
</node>
</node>
</taxNodes>
</record>
</outer>
Desired Output:
<outer>
<node>
<id>1</id>
<note>First level</note>
<node>
<id>node2a</id>
<note>Second level Entry A</note>
<records>
<record>
<id>rec1</id>
</record>
</records>
</node>
<node>
<id>node2b</id>
<note>Second level Entry B</note>
<records>
<record>
<id>rec3</id>
</record>
<record>
<id>rec4</id>
</record>
</records>
</node>
</node>
</outer>
I think this can be seen as a grouping problem and then solved using a recursive function using xsl:for-each-group:
<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"
xmlns:mf="http://example.com/mf"
exclude-result-prefixes="xs math mf"
version="3.0">
<xsl:output indent="yes"/>
<xsl:function name="mf:group" as="node()*">
<xsl:param name="input-nodes" as="element(node)*"/>
<xsl:for-each-group select="$input-nodes" group-by="id">
<xsl:copy>
<xsl:copy-of select="id, note"/>
<xsl:choose>
<xsl:when test="current-group()/node">
<xsl:sequence select="mf:group(current-group()/node)"/>
</xsl:when>
<xsl:otherwise>
<records>
<xsl:apply-templates select="current-group()/ancestor::record"/>
</records>
</xsl:otherwise>
</xsl:choose>
</xsl:copy>
</xsl:for-each-group>
</xsl:function>
<xsl:template match="outer">
<xsl:copy>
<xsl:sequence select="mf:group(record/taxNodes/node)"/>
</xsl:copy>
</xsl:template>
<xsl:template match="record">
<xsl:copy>
<xsl:copy-of select="id"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
That gives the desired result I think for the input sample you have posted, I am not sure it will do for other inputs, mainly as I am not sure how variable the input can be, I think if I understand the spec 'there is only one each of the taxonomic levels "a", "b", and "c"' correctly then it should work fine.
As for having a huge input file and using XSLT 3.0 (with streaming?), I am not sure that a streaming solution is possible, due to the nature of the problem where we need to recursively group the whole set of input nodes.
This stylesheet provides a solution but does so using a function for looping.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:local="http://www.local.com"
exclude-result-prefixes="xs local"
version="2.0">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<xsl:variable name="maxTaxonomyDepth" select="max(//node[not(node)]/count(ancestor-or-self::node))" as="xs:integer"/>
<xsl:sequence select="local:getTaxNodesForDepth(outer, 1, $maxTaxonomyDepth, '')" />
</xsl:template>
<xsl:function name="local:getTaxNodesForDepth">
<xsl:param name="out" as="element()" />
<xsl:param name="curDepth" as="xs:integer" />
<xsl:param name="maxDepth" as="xs:integer" />
<xsl:param name="parentId" as="xs:string*" />
<xsl:for-each select="distinct-values($out/record/taxNodes//node[count(ancestor-or-self::node) = $curDepth]
[if ($curDepth > 1) then parent::node/id/normalize-space(.) = $parentId else true()]
/id/normalize-space(.))">
<xsl:variable name="context" select="." as="xs:string" />
<node>
<xsl:sequence select="($out/record/taxNodes//node[count(ancestor-or-self::node) = $curDepth][id/normalize-space(.) = $context])[1]/(id | descriptor)" />
<xsl:apply-templates select="$out/record[taxNodes/descendant::node[last()][id/normalize-space(.) = $context]]" />
<xsl:choose>
<xsl:when test="$curDepth < $maxDepth">
<xsl:sequence select="local:getTaxNodesForDepth($out, $curDepth + 1, $maxDepth,
($out/record/taxNodes//node[count(ancestor-or-self::node) = $curDepth][id/normalize-space(.) = $context])[1]/id/normalize-space(.))" />
</xsl:when>
</xsl:choose>
</node>
</xsl:for-each>
</xsl:function>
<xsl:template match="taxNodes"/>
<xsl:template match="#*|node()" mode="#default">
<xsl:copy>
<xsl:apply-templates select="#*" mode="#current"/>
<xsl:apply-templates select="node()" mode="#current"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>

sorting elements by attribute on a simple nested structure

I'm still struggling to get my head around a lot of XSLT but have a specific question.
I have a simple nested structure that I want to sort by an attribute (name).
The file has a single root node and then a series of nested nodes. I need to have all the nodes under root sorted within the level they are. The hierarchy is nested to an unspecified level.
The input:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<node name="A">
<node name="C"/>
<node name="B"/>
</node>
<node name="F"/>
<node name="E"/>
</root>
Needs to be transformed into:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<node name="A">
<node name="B"/>
<node name="C"/>
</node>
<node name="E"/>
<node name="F"/>
</root>
I won't bore you with my feable attempts at solving this.
Assuming you do wish the elements to stay within the level they are currently in, firstly, you would need a template to match any element
<xsl:template match="*">
Then you would use xsl:copy to copy the element, and xsl:copy-of to copy any attributes
<xsl:copy>
<xsl:copy-of select="#*"/>
... more code here...
</xsl:copy>
And within the xsl:copy you would then use xsl:apply-templates to process the child elements, along with xsl:sort to select the order
<xsl:apply-templates select="*">
<xsl:sort select="#name" />
</xsl:apply-templates>
Put this altogether gives you this
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes" />
<xsl:template match="*">
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:apply-templates select="*">
<xsl:sort select="#name" />
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
When applied to your input XML the following is output
<root>
<node name="A">
<node name="B"/>
<node name="C"/>
</node>
<node name="E"/>
<node name="F"/>
</root>
This answer is similar to Tim C's, but is just using an identity transform with an xsl:sort. This way you don't loose comments or processing instructions if they're present.
XSLT 1.0
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()">
<xsl:sort select="#name"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>

sum of nodes after blank record until the next blank record using xslt 1.0

I am working in a project where I need to calculate the sum of hours after blank hours until the next blank hours and display them as in the output.
Here is the Input:
<Nodes>
<Node>
<EmpId>1<EmpId>
<InTime></InTime>
<Hours></Hours>
</Node>
<Node>
<EmpId>1<EmpId>
<InTime>10/12/2010</InTime>
<Hours>5</Hours>
</Node>
<Node>
<EmpId>1<EmpId>
<InTime>10/13/2010</InTime>
<Hours>5</Hours>
</Node>
<Node>
<EmpId>1<EmpId>
<InTime></InTime>
<Hours></Hours>
</Node>
<Node>
<EmpId>1</EmpId>
<InTime></InTime>
<Hours></Hours>
</Node>
<Node>
<EmpId>1</EmpId>
<InTime>10/14/2010</InTime>
<Hours>2</Hours>
</Node>
<Node>
<EmpId>1</EmpId>
<InTime>10/14/2010</InTime>
<Hours>3</Hours>
</Node>
</Nodes>
Output should be like:
<Nodes>
<Detail>
<EmpId>1</EmpId>
<InTime>10/12/2010</InTime>
<Hours>10</Hours>
</Detail>
<Detail>
<EmpId>1</EmpId>
<InTime>10/14/2010</InTime>
<Hours>5</Hours>
</Detail>
</Nodes>
Appreciate if any one could help me on this.
Your input XML is malformed (several <EmpId> tags where you should have </EmpId>), but once that's fixed, I believe this does what you describe:
<?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" indent="yes"/>
<xsl:template match="/Nodes">
<Nodes>
<xsl:apply-templates select="Node[Hours != '' and not(normalize-space(preceding-sibling::Node[1]/Hours))]" />
</Nodes>
</xsl:template>
<xsl:template match="Node">
<Detail>
<xsl:copy-of select="EmpId | InTime"/>
<Hours>
<xsl:apply-templates select="." mode="SumHours" />
</Hours>
</Detail>
</xsl:template>
<xsl:template match="Node[normalize-space(following-sibling::Node[1]/Hours)]" mode="SumHours">
<xsl:param name="total" select="0" />
<xsl:apply-templates select="following-sibling::Node[1]" mode="SumHours">
<xsl:with-param name="total" select="$total + Hours" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Node" mode="SumHours">
<xsl:param name="total" select="0" />
<xsl:value-of select="$total + Hours"/>
</xsl:template>
</xsl:stylesheet>

Get a collection of the attributes of a nodeset

I have a collection of nodes like this
<node id="1">
<languaje>c</languaje>
<os>linux</os>
</node>
<node id="2">
<languaje>c++</languaje>
<os>linux</os>
</node>
<node id="3">
<languaje>c#</languaje>
<os>window</os>
</node>
<node id="4">
<languaje>basic</languaje>
<os>mac</os>
</node>
And i want to create a new collection of all the properties id's like this
<root>
<token>1</token>
<token>2</token>
<token>3</token>
<token>4</token>
</root>
How can do that
If you can use XQuery you can do it like this:
<root>
{ ($document/node/<node>{string(#id)}</node>) }
</root>
which is imho the clearest solution.
Otherwise you could create a string (not a document) containing your desired result with XPath 2 by concatenating the tags and your ids :
concat("<root>", string-join(for $i in /base/node/#id return concat("<node>",$i,"</node>"), " ") , "</root>")
All you need is
<xsl:output indent="yes"/>
<xsl:template match="*[node]">
<root>
<xsl:apply-templates select="node"/>
</root>
</xsl:template>
<xsl:template match="node">
<token><xsl:value-of select="#id"/></token>
</xsl:template>
If you want to store the result in a variable you can create a result tree fragment with XSLT 1.0 with e.g.
<xsl:variable name="rtf1">
<xsl:apply-templates select="node()" mode="m1"/>
</xsl:variable>
<xsl:template match="*[node]" mode="m1">
<root>
<xsl:apply-templates select="node" mode="m1"/>
</root>
</xsl:template>
<xsl:template match="node" mode="m1">
<token><xsl:value-of select="#id"/></token>
</xsl:template>
Then you can do <xsl:copy-of select="$rtf1"/> to use the result tree fragment, or with 'exsl:node-set` you can process the created nodes with XPath and XSLT e.g.
<xsl:apply-templates select="exsl:node-set($rtf1)/root/token"/>
With XSLT 2.0 there are no longer result tree fragments so you can use the variable like any input without the need for an extension function.
If you wrap all the nodes under a tag, like <nodes> this works:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<root>
<xsl:apply-templates select="*" />
</root>
</xsl:template>
<!-- templates -->
<xsl:template match="node">
<token><xsl:value-of select="#id" /></token>
</xsl:template>
</xsl:stylesheet>
Tested on XsltCake
http://www.xsltcake.com/slices/E937yH

Using xsl param (if exists) to replcae attribute value

I would like an xsl that replaces the value attribute of the data elements only if the relevant param names are passed (they are passed from the calling java program).
Input
<applicationVariables applicationServer="tomcat">
<data name="HOST" value="localhost"/>
<data name="PORT" value="8080"/>
<data name="SIZE" value="1000"/>
</applicationVariables>
So for example if passing in a param HOST1=myHost and PORT=9080 the output should be:
<applicationVariables applicationServer="tomcat">
<data name="HOST" value="myHost"/>
<data name="PORT" value="9080"/>
<data name="SIZE" value="1000"/>
</applicationVariables>
Note that HOST and PORT where replaced but SIZE was not replaced because there was no parameter with name SIZE
I don't want a hardcoded check for each name, as below:
<xsl:when test="not($HOST)"> <!-- parameter has not been supplied -->
<xsl:attribute name="value"><xsl:value-of select="#value"/></xsl:attribute>
</xsl:when>
<xsl:otherwise> <!--parameter has been supplied -->
<xsl:attribute name="value"><xsl:value-of select="$HOST"/></xsl:attribute>
</xsl:otherwise>
I want a generic way of saying: replace the value attribute only if a param with the same name exists.
But how do i check if a param with name = #name exists?
In such case it is much better to pass all params as elements of a single <xsl:param>:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:param name="pParams">
<p name="HOST">myHost</p>
<p name="PORT">9080</p>
</xsl:param>
<xsl:variable name="vParams" select=
"document('')/*/xsl:param[#name='pParams']/*"/>
<xsl:template match="node()|#*" name="identity">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="#value">
<xsl:attribute name="value">
<xsl:value-of select=
"$vParams[#name=current()/../#name]
|
current()[not($vParams[#name=current()/../#name])]
"/>
</xsl:attribute>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<applicationVariables applicationServer="tomcat">
<data name="HOST" value="localhost"/>
<data name="PORT" value="8080"/>
<data name="SIZE" value="1000"/>
</applicationVariables>
the wanted, correct result is produced:
<applicationVariables applicationServer="tomcat">
<data name="HOST" value="myHost"></data>
<data name="PORT" value="9080"></data>
<data name="SIZE" value="1000"></data>
</applicationVariables>
You can combine logical conditions with xsl:if.