16

I have an input string which has csv values. Eg., 1,2,3 I would need to separate each values and assign to target node in for-each loop.

I got this below template that splits the input string based on delimiter. How can I assign each of the delimited values to the target element in for-each loop.

<xsl:template name="output-tokens">
<xsl:param name="list"/>
<xsl:param name="delimiter"/>
<xsl:variable name="newlist">
  <xsl:choose>
    <xsl:when test="contains($list, $delimiter)">
      <xsl:value-of select="normalize-space($list)"/>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="concat(normalize-space($list), $delimiter)"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:variable>
<xsl:variable name="first" select="substring-before($newlist, $delimiter)"/>
<xsl:variable name="remaining"
              select="substring-after($newlist, $delimiter)"/>
<xsl:variable name="count" select="position()"/>
<num>
  <xsl:value-of select="$first"/>
</num>
<xsl:if test="$remaining">
  <xsl:call-template name="output-tokens">
    <xsl:with-param name="list" select="$remaining"/>
    <xsl:with-param name="delimiter">
      <xsl:value-of select="$delimiter"/>
    </xsl:with-param>
  </xsl:call-template>
</xsl:if>
</xsl:template>

Input xml:

<out1:AvailableDates>
<out1:AvailableDate>15/12/2011,16/12/2011,19/12/2011,20/12/2011,21/12/2011</out1:AvailableDate>
</out1:AvailableDates>

Expected Output:

<tns:AvailableDates>
<tns:AvailableDate>15/12/2011</tns:AvailableDate>
<tns:AvailableDate>16/12/2011</tns:AvailableDate>
<tns:AvailableDate>120/12/2011</tns:AvailableDate>
</tns:AvailableDates>
Arun
  • 405
  • 1
  • 5
  • 11

3 Answers3

21

Here is a complete and short, true XSLT 1.0 solution:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:out1="undefined" xmlns:tns="tns:tns"
  exclude-result-prefixes="out1 tns">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:template match="out1:AvailableDate">
  <tns:AvailableDates>
    <xsl:apply-templates/>
  </tns:AvailableDates>
 </xsl:template>

 <xsl:template match="text()" name="split">
  <xsl:param name="pText" select="."/>
  <xsl:param name="pItemElementName" select="'tns:AvailableDate'"/>
  <xsl:param name="pItemElementNamespace" select="'tns:tns'"/>

    <xsl:if test="string-length($pText) > 0">
     <xsl:variable name="vNextItem" select=
      "substring-before(concat($pText, ','), ',')"/>

      <xsl:element name="{$pItemElementName}"
                   namespace="{$pItemElementNamespace}">
       <xsl:value-of select="$vNextItem"/>
      </xsl:element>

      <xsl:call-template name="split">
        <xsl:with-param name="pText" select=
                       "substring-after($pText, ',')"/>
        <xsl:with-param name="pItemElementName" select="$pItemElementName"/>
        <xsl:with-param name="pItemElementNamespace" select="$pItemElementNamespace"/>
      </xsl:call-template>
    </xsl:if>
 </xsl:template>
</xsl:stylesheet>

when applied on the provided XML document (corrected to be made well-formed):

<out1:AvailableDates xmlns:out1="undefined">
    <out1:AvailableDate>15/12/2011,16/12/2011,19/12/2011,20/12/2011,21/12/2011</out1:AvailableDate>
</out1:AvailableDates>

the wanted, correct result is produced:

<tns:AvailableDates xmlns:tns="tns:tns">
   <tns:AvailableDate>15/12/2011</tns:AvailableDate>
   <tns:AvailableDate>16/12/2011</tns:AvailableDate>
   <tns:AvailableDate>19/12/2011</tns:AvailableDate>
   <tns:AvailableDate>20/12/2011</tns:AvailableDate>
   <tns:AvailableDate>21/12/2011</tns:AvailableDate>
</tns:AvailableDates>
Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431
  • @Arun: I am glad my answer was useful. Could you, please, mark the answer as accepted (click on the check-mark next to the answer)? This is the officially established way of expressing gratitude at SO. – Dimitre Novatchev Dec 14 '11 at 17:41
  • Definitly. In my XSL I am getting the mesage: and are the only permitted – Arun Dec 14 '11 at 18:11
  • @Arun: THis message means that in your `xsl:template` instruction you have used an attribute other than `match` or `name`. Find what other attribute you have used and remove it. – Dimitre Novatchev Dec 14 '11 at 19:07
11

With XSLT 2.0 you can use tokenize(string, separator) function instead of named template.

And this xsl:

<xsl:stylesheet version="2.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:tns="http://tnsnamespace">

    <xsl:template match="AvailableDate">
        <tns:AvailableDates>
            <xsl:for-each select="tokenize(current(), ',')">
                <tns:AvailableDate>
                    <xsl:value-of select="."/>
                </tns:AvailableDate>
            </xsl:for-each>
        </tns:AvailableDates>
    </xsl:template>
</xsl:stylesheet>

gives following result:

<?xml version="1.0" encoding="UTF-8"?>
<tns:AvailableDates xmlns:tns="http://tnsnamespace">
    <tns:AvailableDate>15/12/2011</tns:AvailableDate>
    <tns:AvailableDate>16/12/2011</tns:AvailableDate>
    <tns:AvailableDate>19/12/2011</tns:AvailableDate>
    <tns:AvailableDate>20/12/2011</tns:AvailableDate>
    <tns:AvailableDate>21/12/2011</tns:AvailableDate>
</tns:AvailableDates>

Update:

With Xslt 2.0 processor under backward compatibility mode following template gives the same result:

<xsl:template match="AvailableDate">
    <tns:AvailableDates>
        <xsl:variable name="myValue">
            <xsl:call-template name="output-tokens">
                <xsl:with-param name="list" select="."/>
                <xsl:with-param name="delimiter" select="','"/>
            </xsl:call-template>
        </xsl:variable>

        <xsl:for-each select="$myValue/node()">
            <tns:AvailableDate>
                <xsl:value-of select="."/>
            </tns:AvailableDate>
        </xsl:for-each>
    </tns:AvailableDates>
</xsl:template>

For Xslt 1.0 - it is not possible simple (with standard functions) access to nodes via variable - see @Dimitre Novatchev answer XSLT 1.0 - Create node set and pass as a parameter

For this purpose XSLT 1.0 processors contains extension function: node-set(...)

For Saxon 6.5 node-set() function is defined in http://icl.com/saxon namespace

So in the case of XSLT 1.0 processors solution would be:

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:exslt="http://exslt.org/common"
    xmlns:out1="http://out1namespace"
    xmlns:tns="http://tnsnamespace"
    exclude-result-prefixes="out1 exslt">
    <xsl:output omit-xml-declaration="yes" indent="yes"/>
    <xsl:strip-space elements="*"/>

    <xsl:template match="out1:AvailableDate">
        <tns:AvailableDates>
            <xsl:variable name="myValue">
                <xsl:call-template name="output-tokens">
                    <xsl:with-param name="list" select="."/>
                    <xsl:with-param name="delimiter" select="','"/>
                </xsl:call-template>
            </xsl:variable>
            <xsl:for-each select="exslt:node-set($myValue)/node()">
                <tns:AvailableDate>
                    <xsl:value-of select="."/>
                </tns:AvailableDate>
            </xsl:for-each>
        </tns:AvailableDates>
    </xsl:template>

    <xsl:template name="output-tokens">
        <xsl:param name="list"/>
        <xsl:param name="delimiter"/>
        <xsl:variable name="newlist">
            <xsl:choose>
                <xsl:when test="contains($list, $delimiter)">
                    <xsl:value-of select="normalize-space($list)"/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:value-of select="concat(normalize-space($list), $delimiter)"/>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:variable>
        <xsl:variable name="first" select="substring-before($newlist, $delimiter)"/>
        <xsl:variable name="remaining"
            select="substring-after($newlist, $delimiter)"/>
        <xsl:variable name="count" select="position()"/>
        <num>
            <xsl:value-of select="$first"/>
        </num>
        <xsl:if test="$remaining">
            <xsl:call-template name="output-tokens">
                <xsl:with-param name="list" select="$remaining"/>
                <xsl:with-param name="delimiter">
                    <xsl:value-of select="$delimiter"/>
                </xsl:with-param>
            </xsl:call-template>
        </xsl:if>
    </xsl:template>

</xsl:stylesheet>

Thanks @Dimitre Novatchev to correct me and his answer about accessing node sets from variable.

Community
  • 1
  • 1
Vitaliy
  • 2,744
  • 1
  • 24
  • 39
  • Thank very much. I am using BPEL 10g. Its having xslt version 1.0. What is possible in it? – Arun Dec 14 '11 at 09:50
  • I've updated my answer for Xslt-1.0 - it uses your *output-tokens* named template – Vitaliy Dec 14 '11 at 10:54
  • @Vitaliy: Please, do run your XSLT 1.0 solution with any XSLT 1.0 (compliant) processor and see that this produces an error. In XSLT 1.0 there are a very limited set of operations that are allowed on an RTF (Result Tree Fragment). Please, correct. – Dimitre Novatchev Dec 14 '11 at 14:08
  • @Dimitre Novatchev - yes, I see it doesn't work with Saxon 6.5.5 (only works when I added version="1.1"), but works with Saxon 9.3 under backward compatibility mode. – Vitaliy Dec 14 '11 at 14:47
  • @Dimitre Novatchev - fixed with node-set() extension function – Vitaliy Dec 15 '11 at 07:51
  • @Vitaliy: Good. I will upvote this answer if you edit it and use the EXSLT namespace (which is supported by the majority of existing XSLT 1.0 processor), instead of the proprietory Saxon namespace. – Dimitre Novatchev Dec 15 '11 at 13:18
0

Personally, I prefer this variant based on custom extension functions. The method is compact and clean, and works fine in XSLT 1.0 (at least with XALAN 2.7 as embedded in any recent JVM).

1) declare a class with a static method returning a org.w3c.dom.Node

package com.reverseXSL.util;

import org.w3c.dom.*;
import java.util.regex.*;
import javax.xml.parsers.DocumentBuilderFactory;

public class XslTools {

  public static Node splitToNodes(String input, String regex) throws Exception {
    Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
    Element item, list = doc.createElement("List");
    Pattern p = Pattern.compile(regex);
    Matcher m = p.matcher(input);
    while (m.find()) {
      item = doc.createElement("Item");
      StringBuffer sb = new StringBuffer();
      for (int i=1; i<=m.groupCount(); ++i) if (m.start(i)>=0) sb.append(m.group(i));
      Text txt = doc.createTextNode(sb.toString());
      item.appendChild(txt);
      list.appendChild(item);
    }
    return list; 
  }

}

This function splits an input string on a regex pattern and creates a document fragment of the kind <list><Item>A</Item><Item>B</Item><Item>C</Item></List>. The regex is matched in sequence, each match yielding an Item element whose value is composed from the capturing groups (some possibly empty) inside each regex match. This allows to get rid from delimiters and other syntax chars.

For instance, to split a comma-separated list like " A, B ,, C", skip empty values, and trim extra spaces (hence get the above Node list), use a regex like '\s*([^,]+?)\s*(?:,|$)' - a mind twisting one! If instead you want to split the input text by a fixed size (here 10 chars) with the last Item taking whatever remains, use a regex like '(.{10}|.+)' - love it!

You can then use the function in XSLT 1.0 as follows (quite compact!):

<xsl:stylesheet version="1.0" xmlns:var="com.reverseXSL.util.XslTools" extension-element-prefixes="var" ...
...
<xsl:template ...
  ...
  <xsl:for-each select="var:splitToNodes(Detail/CsvText,'\s*([^,]+?)\s*(?:,|$)')/Item">
    <Loop><xsl:value-of select="."/></Loop>
  </xsl:for-each>
...

Executed on a template match yielding the input fragment <Detail><CsvText>a, b ,c </CsvText></Detail> you'll generate <Loop>a</Loop><Loop>b</Loop><Loop>c</Loop>

The trick is not forgetting to follow the function call that generates the Node/Item by the XPath "/Item" (or "/*") as you shall note, so that a Node sequence is returned into the for-each loop.

Bernard Hauzeur
  • 2,317
  • 1
  • 18
  • 25
  • 1
    If you're working with Xalan, then you can use the EXSLT `str:tokenize()` extension function which Xalan supports, instead of having to write your own. But XSLT 1.0 does not necessarily mean you're using Xalan or Java. – michael.hor257k Jun 14 '17 at 21:45