Comma separated string parsing XSLT to for-each node

后端 未结 3 1382
無奈伤痛
無奈伤痛 2021-02-07 12:59

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 s

相关标签:
3条回答
  • 2021-02-07 13:14

    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>
    
    0 讨论(0)
  • 2021-02-07 13:18

    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.

    0 讨论(0)
  • 2021-02-07 13:38

    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.

    0 讨论(0)
提交回复
热议问题