XSLT 2.0 performance of convert flat XML data to heirarchical? - xslt-2.0

I have data as follows:
<Root>
<Node>
<Region>Central</Region>
<Provider>A</Provider>
<Value>100</Value>
</Node>
<Node>
<Region>Central</Region>
<Provider>B</Provider>
<Value>200</Value>
</Node
<Node>
<Region>Central</Region>
<Provider>B</Provider>
<Value>250</Value>
</Node>
<Node>
<Region>Eastern</Region>
<Provider>C</Provider>
<Value>50</Value>
</Node>
</Root>
And trying to transform as:
<Root>
<Region>
<Name>Central</Name>
<Provider>
<Name>A</Name>
<Item>
<Value>100</Value>
</Item>
</Provider>
<Provider>
<Name>B</Name>
<Item>
<Value>200</Value>
</Item>
<Item>
<Value>250</Value>
</Item>
</Provider>
</Region>
<Region>
<Name>Eastern</Name>
<Provider>
<Name>C</Name>
<Item>
<Value>50</Value>
</Item>
</Provider>
</Region>
</Root>
In the past I have used Muenchian Method, but I am trying to use XSLT 2.0 construct xsl:for-each-group to accomplish the same thing without success so far.
Is it possible to use xsl:for-each-group to accomplish the above? How?
Edit
This is what I have so far, which appears to work. But, I am not sure about efficiency - specifically does using such constructs force the parser to read the entire document and sort it? The data coming in is already sorted the way I want.
<xsl:template match="Root">
<xsl:element name="Root">
<xsl:for-each-group select="Node" group-by="Region">
<xsl:element name="Region">
<xsl:element name="Name">
<xsl:value-of select="Region" />
</xsl:element>
<xsl:for-each-group select="current-group()" group-by="Provider">
<xsl:element name="Provider">
<xsl:for-each select="current-group()">
<xsl:element name="Item">
<xsl:value-of select="Value" />
</xsl:element>
</xsl:for-each>
</xsl:element>
</xsl:for-each-group>
</xsl:element>
</xsl:for-each-group>
</xsl:element>
</xsl:template>

Related

Transform only the last element of XML and copy the rest in XSLT

I have an XML like below -
<root>
<row>
<col1>16</col1>
<col2>466</col2>
<col3>144922</col3>
<col4>0</col4>
<col5>5668</col5>
<col6>475</col6>
</row>
</root>
The number of columns can vary inside the root element. It can also be up to col9. My requirement is to modify the last column and copy others as it is for an incoming XML.
I have something like this till now where I am assigning the value to used as the last element in a variable and then trying to call it when the last position is reached-
<?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="xs"
version="2.0">
<xsl:param name="line88.field2" />
<xsl:param name="rec16.col2" />
<xsl:variable name="col3">
<xsl:choose>
<xsl:when test="$rec16.col2 ='165'">
<xsl:value-of select="'Y'"/>
</xsl:when>
<xsl:when>
------
<xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"></xsl:apply-templates>
</xsl:copy>
</xsl:template>
<xsl:template match="row[position() = last()]">
<col9>
<xsl:call-template name="AnotherTemplate">
<xsl:with-param name="inputData">
<xsl:value-of select="$col3" />
</xsl:with-param>
</xsl:call-template>
</col9>
</xsl:template>
<xsl:template name="AnotherTemplate">
<xsl:param name="inputData"></xsl:param>
<xsl:value-of select="$inputData" />
</xsl:template>
</xsl:stylesheet>
But this is not working for me. Just giving me one column with the modified value.Please help.
The desired outcome should be as below where the last column has the value from the variable.
<root>
<row>
<col1>16</col1>
<col2>466</col2>
<col3>144922</col3>
<col4>0</col4>
<col5>5668</col5>
<col6>Y</col6>
</row>
</root>
Without knowing your whole XSLT code. You can use this row template:
<xsl:template match="row/*[starts-with(local-name(),'col') and position() = last()]">
<xsl:element name="{concat('col',position() div 2)}">
<xsl:call-template name="AnotherTemplate">
<xsl:with-param name="inputData">
<xsl:value-of select="$col3" />
</xsl:with-param>
</xsl:call-template>
</xsl:element>
</xsl:template>
It replaces the last col? element by the given value (the result of the xsl:call-template code).

How to access previous and next item based on a condition

I have 2000 TEI-XML-files with letters between different people and a single person in a single folder. I can access the previous and next letter chronologically as the filename starts with the date of the sender (e.g. 2001-02-21.xml). But what I want to achieve is to create an XSLT (2 or 3, doesn't matter) that writes the next letter to or from the specific writer/receiver into the xml-file.
Say I have this:
<correspDesc>
<correspAction type="sent">
<persName key="CMvW">Carl Maria von Weber</persName>
<settlement>Dresden</settlement>
<date when="1817-06-23">23 June 1817</date>
</correspAction>
<correspAction type="received">
<persName key="CB">Caroline Brandt</persName>
<settlement>Prag</settlement>
</correspAction>
</correspDesc>
and i want to add this field as a third child after correspAction:
<correspContext>
<ref type="prev"
target="http://www.weber-gesamtausgabe.de/A041209">Previous letter of
<persName key="CMvW">Carl Maria von Weber</persName>
to <persName key="CB">Caroline Brandt</persName>:
<date from="1817-06-19" to="1817-06-20">June 19/20, 1817</date>
</ref>
<ref type="next"
target="http://www.weber-gesamtausgabe.de/A041217">Next letter of
<persName key="CMvW">Carl Maria von Weber</persName> to
<persName key="CB">Caroline Brandt</persName>:
<date when="1817-06-27">June 27, 1817</date>
</ref>
</correspContext>
how would I do it? In the example Caroline Brandt is the changing sender/receiver. So basically I would need a collection of all XML-files with //correspDesc//persName[#key='CB'] and in each file access the preceding and following one of the collection. How can I achieve that?
begin of the solution
#martin-honnen pointed me the way though I'm certain it is not the most elegant way.
1) I've used collection to copy all correspDesc into one file. I add an attribute 'lookup' with the key of the person and the date the letter was sent combined and an attribute with just 'person' to identify the letters of a correspondence.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0">
<xsl:mode on-no-match="shallow-copy"/>
<xsl:output indent="yes" method="xml" encoding="utf-8" omit-xml-declaration="false"/>
<xsl:template match="/">
<xsl:element name="root">
<xsl:for-each select="collection('?select=*.xml;recurse=no')">
<xsl:element name="correspDesc">
<xsl:attribute name="lookup">
<xsl:choose>
<xsl:when test="//correspAction[#type='sent']/persName/#key='pmb2121'">
<xsl:value-of select="//correspAction[#type='received']/persName/#key"/>
<xsl:text>|</xsl:text>
<xsl:value-of select="//correspAction[#type='sent']/date/#when"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="//correspAction[#type='sent']/persName/#key"/>
<xsl:text>|</xsl:text>
<xsl:value-of select="//correspAction[#type='sent']/date/#when"/>
</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<xsl:attribute name="person">
<xsl:choose>
<xsl:when test="//correspAction[#type='sent']/persName/#key='pmb2121'">
<xsl:value-of select="//correspAction[#type='received']/persName/#key"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="//correspAction[#type='sent']/persName/#key"/>
</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<xsl:apply-templates select="//correspAction"/>
</xsl:element>
</xsl:for-each>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
2) I order the resulting list using the date-field
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0">
<xsl:mode on-no-match="shallow-copy" />
<xsl:output indent="yes"
method="xml"
encoding="utf-8"
omit-xml-declaration="false"/>
<xsl:template match="root">
<xsl:element name="root">
<xsl:apply-templates select="correspDesc">
<xsl:sort select="correspAction[#type='sent']/date/#when" data-type="text" order="ascending"/>
</xsl:apply-templates>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
Now my next task is to use param and key to lookup the preceding and following entries.
<xsl:param name="correspList" select="document('correspList.xml')"/>
<xsl:key name="corresp-lookup" match="#lookup"/>
<xsl:key name="correspPerson-lookup" match="#person"/>
I am not sure how I will achieve that but I will update here once I have that code.

XSLT Merge two same elements in one

I have been trying to merge two same elements under one using XSLT 2.0
Sample source XML:
<?xml version="1.0" encoding="UTF-8"?>
<summary>
<object>
<para>Paragraph <ref>Test1.</ref>AAA</para>
<para>Test2.</para>
</object>
<objects>
<para>
<title>Title 1</title>: (1) Testing1</para>
<para>(2) Testing 2</para>
<para>Testing 3</para>
</objects>
<objects>
<para>
<title>Title 2</title>: Testing 4</para>
</objects>
Desired output would be:
<summary>
<object>
<para>Paragraph <ref>Test1.</ref>AAA</para>
<para>Test2.</para>
</object>
<objects>
<para>
<title>Title 1</title>: (1) Testing1</para>
<para>(2) Testing 2</para>
<para>Testing 3</para>
<para>
<title>Title 2</title>: Testing 4</para>
</objects>
</summary>
I use the following template for the transformation unfortunately it is not giving me desired result..
<xsl:template match="summary">
<xsl:for-each select="//objects">
<xsl:element name="objects">
<xsl:for-each select="//objects/*">
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:element>
</xsl:for-each>
</xsl:template>
<xsl:template match="*|#*|comment()|processing-instruction()|text()" >
<xsl:copy copy-namespaces="no">
<xsl:apply-templates select="*|#*|comment()|processing-instruction()|text()" />
</xsl:copy>
</xsl:template>
If you simply want to merge all objects sibling elements then
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="objects[1]">
<xsl:copy>
<xsl:copy-of select="node(), following-sibling::objects/node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="objects[position() gt 1]"/>
</xsl:transform>
should suffice. For merging only adjacent siblings you could use <xsl:for-each-group select="*" group-adjacent="boolean(self::objects)">...</xsl:for-each-group> in a template matching summary or any objects parent.

Use a dynamic match in XSLT

I have an external document with a list of multiple Xpath like this:
<EncrypRqField>
<EncrypFieldRqXPath01>xpath1</EncrypFieldRqXPath01>
<EncrypFieldRqXPath02>xpath2</EncrypFieldRqXPath02>
</EncrypRqField>
I use this document to obtain the Xpath of the nodes I want to be modified.
The input XML is:
<Employees>
<Employee>
<id>1</id>
<firstname>xyz</firstname>
<lastname>abc</lastname>
<age>32</age>
<department>xyz</department>
</Employee>
</Employees>
I want to obtain something like this:
<Employees>
<Employee>
<id>XXX</id>
<firstname>xyz</firstname>
<lastname>abc</lastname>
<age>XXX</age>
<department>xyz</department>
</Employee>
</Employees>
The XXX values are the result of a data encryption, I want to dynamically obtain the Xpath from the document and change the value of its node.
Thanks.
I'm not sure if something like this is possible in XSL 2.0. May be in 3.0 there should be some function evaluate() but I don't know any details.
But I tried some workaround and it seems to be functional. Of course it is not perfect and has many limitations in this form (e.g. you need to specify absolute path, you cannot use more complex XPath like //, [], etc.) so consider it just as an idea. But it could be the way in some easier cases.
It is based on comparing of two string instead of evaluation string as XPath.
Simplified xml with xpaths to encrypt (I ommit the number for simplicity).
<?xml version="1.0" encoding="UTF-8"?>
<EncrypRqField>
<EncrypFieldRqXPath>/Employees/Employee/id</EncrypFieldRqXPath>
<EncrypFieldRqXPath>/Employees/Employee/age</EncrypFieldRqXPath>
</EncrypRqField>
And my transformation
<xsl:template match="element()">
<xsl:variable name="pathToElement">
<xsl:call-template name="getPath">
<xsl:with-param name="element" select="." />
</xsl:call-template>
</xsl:variable>
<xsl:choose>
<xsl:when test="$xpaths/EncrypFieldRqXPath[text() = $pathToElement]">
<!-- If exists element with exacty same value as constructed "XPath", ten "encrypt" the content of element -->
<xsl:copy>
<xsl:text>XXX</xsl:text>
</xsl:copy>
</xsl:when>
<xsl:otherwise>
<xsl:copy>
<xsl:apply-templates />
</xsl:copy>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- This template will "construct" the XPath for element under investigation. -->
<!-- There might be an easier way (e.g. some build-in function), but it is actually out of my skill. -->
<xsl:template name="getPath">
<xsl:param name="element" />
<xsl:choose>
<xsl:when test="$element/parent::node()">
<xsl:call-template name="getPath">
<xsl:with-param name="element" select="$element/parent::node()" />
</xsl:call-template>
<xsl:text>/</xsl:text>
<xsl:value-of select="$element/name()" />
</xsl:when>
<xsl:otherwise />
</xsl:choose>
</xsl:template>
</xsl:stylesheet>

Complex XSL Transformation

I am still a beginner with XSLT but I am having a difficult task in hand.
I have a non-xml file which needs to be transformed. The format of the file is a s follows:
type1
type1line1
type1line2
type1line3
type2
type2line1
type2line2
type3
type3line1
type3line2
types (type1, type2, ...) are specified using certain codes which don't have a specific order. Each type has multiple line underneath.
So, I need to transform this file but the problem is that for each type I have to do a different transformation for each of it's underlying lines.
Now, I can read the string line by line and determine that a new type has begun but I don't know how to set a flag (indicating the type) to use it in the underlying lines.
Here is what I have right now:
<?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" version="2.0">
<xsl:param name="testString" as="xs:string">
type1
line1
line2
type1
line1
</xsl:param>
<xsl:template match="/">
<xsl:call-template name="main">
<xsl:with-param name="testString" select="$testString"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="main">
<xsl:param name="testString"/>
<xsl:variable name="iniFile" select="$testString"/>
<config>
<xsl:analyze-string select="$iniFile" regex="\n">
<xsl:non-matching-substring>
<item>
<xsl:choose>
<xsl:when test="starts-with(., 'type1')">
<!-- do a specific transformation-->
</xsl:when>
<xsl:when test="starts-with(., 'type2')">
<!-- do another transformation-->
</xsl:when>
</xsl:choose>
</item>
</xsl:non-matching-substring>
</xsl:analyze-string>
</config>
</xsl:template>
</xsl:stylesheet>
Any idea about how to solve the problem.
I think XSLT 2.1 will allow you to use its powerful stuff like for-each-group on sequences of atomic values like strings but with XSLT 2.0 you have such powerful features only for sequences of nodes so my first step when using XSLT 2.0 with plain string data I want to process/group is to create elements. So you could tokenize your data, wrap each token into some element and then use for-each-group group-starting-with to process each group starting with some pattern like '^type[0-9]+$'.
You haven't really told us what you want to with the data once you have identified a group so take the following as an example you could adapt:
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs">
<xsl:output method="xml" indent="yes"/>
<xsl:param name="input" as="xs:string">type1
type1line1
type1line2
type1line3
type2
type2line1
type2line2
type3
type3line1
type3line2</xsl:param>
<xsl:template name="main">
<xsl:variable name="lines" as="element(item)*">
<xsl:for-each select="tokenize($input, '\n')">
<item><xsl:value-of select="."/></item>
</xsl:for-each>
</xsl:variable>
<xsl:for-each-group select="$lines" group-starting-with="item[matches(., '^type[0-9]+$')]">
<xsl:choose>
<xsl:when test=". = 'type1'">
<xsl:apply-templates select="current-group() except ." mode="m1"/>
</xsl:when>
<xsl:when test=". = 'type2'">
<xsl:apply-templates select="current-group() except ." mode="m2"/>
</xsl:when>
<xsl:when test=". = 'type3'">
<xsl:apply-templates select="current-group() except ." mode="m3"/>
</xsl:when>
</xsl:choose>
</xsl:for-each-group>
</xsl:template>
<xsl:template match="item" mode="m1">
<foo>
<xsl:value-of select="."/>
</foo>
</xsl:template>
<xsl:template match="item" mode="m2">
<bar>
<xsl:value-of select="."/>
</bar>
</xsl:template>
<xsl:template match="item" mode="m3">
<baz>
<xsl:value-of select="."/>
</baz>
</xsl:template>
</xsl:stylesheet>
When applied with Saxon 9 (command line options -it:main -xsl:sheet.xsl) the result is
<foo>type1line1</foo>
<foo>type1line2</foo>
<foo>type1line3</foo>
<bar>type2line1</bar>
<bar>type2line2</bar>
<baz>type3line1</baz>
<baz>type3line2</baz>

Resources