Last week I helped someone on the Oracle community forums
with transforming a comma separated string to a list of elements. He needed this to process each element in BPM Suite, but it is a use case that can come around in SOA Suite or even in Oracle Integration Cloud.
You would think that you could do something like a for-each and trimming the element from the variable.
Recursion
One typical thing with XSLT is that variables are
immutable. That means that you can declare a variable and assign a value to it, but you cannot change it. So it is not possible to assign a new value to a variable based on a substring of that same variable.
To circumvent this, you should implement a template that conditionally calls itself until an end-condition is met. This is a typical algorithm called
recursion. Recursion is a way of implementing a function that calls itself, for example to calculate the faculty of a number. Recursion can help circumventing the immutability of variables, because with every call to the function you can pass (a) calculated and thus different value(s) through the parameter(s).
I wrote about this
earlier, but last week a co-worker asked a similar question, but just the other way around: transforming a list into a comma separated string.
So, apparently it's time to write an article about it.
Transforming CSV to a List
I refactored the xsd's from the question as follows. First the source xsd:
<?xml version="1.0" encoding="windows-1252" ?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://www.example.org/Approvals/Source"
targetNamespace="http://www.example.org/Approvals/Source" elementFormDefault="qualified">
<xsd:element name="ApprovalRoute" type="tns:approvalRouteByInvoiceNatureResponse"/>
<xsd:complexType name="approvalRouteByInvoiceNatureResponse">
<xsd:sequence>
<xsd:element type="xsd:string" name="approvalRoute" minOccurs="0"/>
<xsd:element type="xsd:boolean" name="autoApprove" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>
And the target schema is:
<?xml version="1.0" encoding="windows-1252" ?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://www.example.org/Approvals/Target"
targetNamespace="http://www.example.org/Approvals/Target" elementFormDefault="qualified">
<xsd:element name="ApprovalRoute" type="tns:ApprovalRouteType"/>
<xsd:complexType name="ApprovalRouteType">
<xsd:sequence>
<xsd:element name="Approver" type="xsd:string" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>
To start with, we have an
ApprovalRoute element based on a complex type with the
approvalRoute sub-element being the comma-separated list of approvers. Then as a target we have an ApprovalRoute, based on a list of Approver elements.
I generated the following source xml to transform:
<?xml version="1.0" encoding="UTF-8" ?>
<ApprovalRoute xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.example.org/Approvals/Source SOA/Schemas/Approvals-Source.xsd"
xmlns="http://www.example.org/Approvals/Source">
<approvalRoute>Approver1,Approver2,Approver3,Approver4,Approver5</approvalRoute>
<autoApprove>true</autoApprove>
</ApprovalRoute>
Now, we need to split the approvalRoute value in a part before the first comma, and after the first comma. The value before the first comma can be put in an element. But the remainder has to be fed into the same
template again. Then, at the end there is no comma in the remainder, so the part before the comma will be empty. There is no comma anymore, so we should not call the template with the remainder, but simply put the remainder in an element. Therefor, the non-existence of the comma can be the end-condition.
Remember, using recursion, you should always have a finalizing condition. To be honest, in my first piece of code in the answer of the question, I forgot about that. But, to my defence: I just put it together by heart and haven't been able to test.
The explanation above results in the following template:
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0"
xmlns:xp20="http://www.oracle.com/XSL/Transform/java/oracle.tip.pc.services.functions.Xpath20"
xmlns:tns="http://www.example.org/Approvals/Target"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ns0="http://www.example.org/Approvals/Source" xmlns:xsl="
http://www.w3.org/1999/XSL/Transform">
<!-- https://community.oracle.com/thread/4178385 -->
<xsl:template match="/">
<tns:ApprovalRoute>
<xsl:call-template name="parseDelimitedString">
<xsl:with-param name="delimitedStr" select="/ns0:ApprovalRoute/ns0:approvalRoute"/>
</xsl:call-template>
</tns:ApprovalRoute>
</xsl:template>
<xsl:template name="parseDelimitedString">
<xsl:param name="delimitedStr"/>
<!-- https://www.w3schools.com/xml/xsl_functions.asp -->
<xsl:variable name="firstItem" select="substring-before($delimitedStr, ',')"/>
<xsl:variable name="restDelimitedStr" select="substring-after($delimitedStr, ',')"/>
<tns:Approver>
<xsl:value-of select="$firstItem"/>
</tns:Approver>
<xsl:choose>
<xsl:when test="contains($restDelimitedStr, ',')">
<xsl:call-template name="parseDelimitedString">
<xsl:with-param name="delimitedStr" select="$restDelimitedStr"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<tns:Approver>
<xsl:value-of select="$restDelimitedStr"/>
</tns:Approver>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
(I created this as an XSL Map, but removed the comments that were included by JDeveloper.
I tested this with the following input:
<?xml version="1.0" encoding="UTF-8" ?>
<ApprovalRoute xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.example.org/Approvals/Source SOA/Schemas/Approvals-Source.xsd"
xmlns="http://www.example.org/Approvals/Source">
<approvalRoute>Approver1,Approver2,Approver3,Approver4,Approver5</approvalRoute>
<autoApprove>true</autoApprove>
</ApprovalRoute>
And this resulted in the following output:
<?xml version = '1.0' encoding = 'UTF-8'?>
<tns:ApprovalRoute xmlns:tns="http://www.example.org/Approvals/Target" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.example.org/Approvals/Target file:/D:/Projects/2018-ODC/XSL-Demo/XSL-Demo/SOA/Schemas/Approvals-Target.xsd">
<tns:Approver>Approver1</tns:Approver>
<tns:Approver>Approver2</tns:Approver>
<tns:Approver>Approver3</tns:Approver>
<tns:Approver>Approver4</tns:Approver>
<tns:Approver>Approver5</tns:Approver>
</tns:ApprovalRoute>
This I used for input for the following xslt.
The other way around: List to CSV
For didactional reasons I'll show the other way around too. Although, we'll see that this can be done easier.
In this case I mean to loop over a series of elements, starting with an index of 1, and adding the elements to a partial string. That means I have 3 parameters:
- loopApprovers: the parent element, containing all the elements to loop over
- index: the loop index, with a default of 1
- partialApprovalRoute: the partial CSV list, defaulted to an empty string
The template
loopApprovers can be called with only the approvalRoute. Then with an index of 1, the template is called recursively the first time, with a
partialApprovalRoute assigned with the first Approver occurence and an
index increased with 1.
For the other occurences where
index > 1 and
index <= count of elements, the template is called again recursively, but with an increased index and the
indexed element added to the
partialApprovalRoute separated with a comma.
Then the end situation is when the template is called where
index exceeds the
count of elements. Then just the
partialApprovalRoute is 'returned' (by the value-of instruction) where it is
substringed to a 20000 characters:
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xp20="http://www.oracle.com/XSL/Transform/java/oracle.tip.pc.services.functions.Xpath20"
xmlns:ns0="http://www.example.org/Approvals/Target"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tns="http://www.example.org/Approvals/Source"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<tns:ApprovalRoute>
<tns:approvalRoute>
<xsl:call-template name="loopApprovers">
<xsl:with-param name="approvalRoute" select="/ns0:ApprovalRoute"/>
</xsl:call-template>
</tns:approvalRoute>
</tns:ApprovalRoute>
</xsl:template>
<xsl:template name="loopApprovers">
<xsl:param name="approvalRoute"/>
<xsl:param name="index" select="1"/>
<xsl:param name="partialApprovalRoute" select="''"/>
<xsl:choose>
<xsl:when test="number($index)=1">
<xsl:call-template name="loopApprovers">
<xsl:with-param name="approvalRoute" select="$approvalRoute"/>
<xsl:with-param name="index" select="$index+1"/>
<xsl:with-param name="partialApprovalRoute" select="$approvalRoute/ns0:Approver[1]"/>
</xsl:call-template>
</xsl:when>
<xsl:when test="number($index)> 1 and number($index)<=count($approvalRoute/ns0:Approver)">
<xsl:call-template name="loopApprovers">
<xsl:with-param name="approvalRoute" select="$approvalRoute"/>
<xsl:with-param name="index" select="$index+1"/>
<xsl:with-param name="partialApprovalRoute"
select="concat($partialApprovalRoute,',',$approvalRoute/ns0:Approver[number($index)])"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="substring($partialApprovalRoute,1,20000)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Simpler transformation from list to csv
As can be found
here for instance, a for-each does not necessarily need to return an element. It can return just a value. So, it can be a bit simpeler:
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xp20="http://www.oracle.com/XSL/Transform/java/oracle.tip.pc.services.functions.Xpath20"
xmlns:ns0="http://www.example.org/Approvals/Target"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tns="http://www.example.org/Approvals/Source"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<!-- http://p2p.wrox.com/xslt/72164-xslt-need-concatenate-strings-loop-hold-them-later-use.html -->
<xsl:template match="/">
<tns:ApprovalRoute>
<tns:approvalRoute>
<xsl:call-template name="loopApprovers">
<xsl:with-param name="approvalRoute" select="/ns0:ApprovalRoute"/>
</xsl:call-template>
</tns:approvalRoute>
</tns:ApprovalRoute>
</xsl:template>
<xsl:template name="loopApprovers">
<xsl:param name="approvalRoute"/>
<xsl:variable name="approvalRouteCsv">
<xsl:for-each select="$approvalRoute/ns0:Approver">
<xsl:value-of select="concat(substring(.,1,20000),',')"/>
</xsl:for-each>
</xsl:variable>
<xsl:value-of select="substring($approvalRouteCsv,1,string-length($approvalRouteCsv)-1)"/>
</xsl:template>
</xsl:stylesheet>
Conclusion
Understanding Recursion with XSLT will help you with solving much complexer problems in transformations. The last example of transforming a list to a comma separated list is of course structural easier. But the recursive variant allows for more calculations or conditional processing.