" /> " /> "/>

Xpath for xml inside cdata section using xslt

85 Views Asked by At

I want to generate xpath for xml element which is inside the CDATA.

INPUT :

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
    <s:Body>
        <CSBIV xmlns="http://tempuri.org/">
            <Response>true</Response>
            <CSBIV_CargoResult><![CDATA[
            <?xml version="1.0" encoding="UTF-8"?>
            <ShipmentRecords>
                <CustomerInfo>
                    <SoftwareVersion>6.0</SoftwareVersion>
                    <DefaultAccountNo>530901961</DefaultAccountNo>
                </CustomerInfo>
            </ShipmentRecords>  
            ]]></CSBIV_CargoResult>
        </CSBIV>
    </s:Body>
</s:Envelope>

XSLT Used to generate xpath:

<xsl:stylesheet version="3.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
    <xsl:strip-space elements="*"/>
    
    <xsl:template match="*">
        <xsl:param name="path"/>
        <xsl:variable name="my-path">
            <xsl:for-each select="ancestor-or-self::*">
                <xsl:value-of select="concat('/',name())"/>
                <xsl:if test="(preceding-sibling::*|following-sibling::*)[name()=name(current())]">
                    <xsl:value-of select="concat('[',count(preceding-sibling::*[name()=name(current())])+1,']')"/>
                </xsl:if>
            </xsl:for-each>
        </xsl:variable>
        
        <xsl:copy>
            <xsl:variable name="elementXpath" select="replace($my-path, '/[a-zA-Z0-9-]*?:|/[a-zA-Z0-9-]*?','/*:')"/>
            <xsl:attribute name="xpath">
                <xsl:value-of select="concat($elementXpath,'/text()')"/>
            </xsl:attribute>
            <xsl:copy-of select="@*"/>
            
            <xsl:for-each select="@*">
                <xsl:attribute name="{name()}" select="concat($elementXpath,'/@',name())" separator=""/>
            </xsl:for-each>
            
            <xsl:apply-templates>
                <xsl:with-param name="path" select="$my-path"/>
            </xsl:apply-templates>
        </xsl:copy>
    </xsl:template>
    
    <xsl:template match="text()"/>
    
</xsl:stylesheet>

So this will give me this output.

<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
            xpath="/*:Envelope/text()">
   <s:Body xpath="/*:Envelope/*:Body/text()">
      <CSBIV xmlns="http://tempuri.org/" xpath="/*:Envelope/*:Body/*:CSBIV/text()">
         <Response xpath="/*:Envelope/*:Body/*:CSBIV/*:Response/text()"/>
         <CSBIV_CargoResult xpath="/*:Envelope/*:Body/*:CSBIV/*:CSBIV_CargoResult/text()"/>
      </CSBIV>
   </s:Body>
</s:Envelope>

So this ignore CDATA section. So I want something like this below as output. I want that it will give xpath for inside xml in CDATA with concating parent xpath with |@|CDATA|@|

<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
    xpath="/*:Envelope/text()">
    <s:Body xpath="/*:Envelope/*:Body/text()">
        <CSBIV xmlns="http://tempuri.org/" xpath="/*:Envelope/*:Body/*:CSBIV/text()">
            <Response xpath="/*:Envelope/*:Body/*:CSBIV/*:Response/text()"/>
            <CSBIV_CargoResult xpath="/*:Envelope/*:Body/*:CSBIV/*:CSBIV_CargoResult/text()">
               <ShipmentRecords xpath="/*:Envelope/*:Body/*:CSBIV/*:CSBIV_CargoResult|@|CDATA|@|/*:ShipmentRecords/text()"> 
                    <CustomerInfo xpath="/*:Envelope/*:Body/*:CSBIV/*:CSBIV_CargoResult|@|CDATA|@|/*:ShipmentRecords/*:CustomerInfo/text()">
                        <SoftwareVersion xpath="/*:Envelope/*:Body/*:CSBIV/*:CSBIV_CargoResult|@|CDATA|@|/*:ShipmentRecords/*:CustomerInfo/*:SoftwareVersion/text()"/>
                        <DefaultAccountNo xpath="/*:Envelope/*:Body/*:CSBIV/*:CSBIV_CargoResult|@|CDATA|@|/*:ShipmentRecords/*:CustomerInfo/*:DefaultAccountNo/text()"/>
                    </CustomerInfo>
                </ShipmentRecords>
            </CSBIV_CargoResult>
        </CSBIV>
    </s:Body>
</s:Envelope>
1

There are 1 best solutions below

6
Martin Honnen On

You can do e.g.

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  exclude-result-prefixes="#all"
  expand-text="yes">
  
  <xsl:template match="*">
    <xsl:param name="path-prefix" as="xs:string" tunnel="yes" select="''"/>
    <xsl:copy>
      <xsl:apply-templates select="@*"/>
      <xsl:attribute name="xpath" select="$path-prefix || path()"/>
      <xsl:apply-templates/>
    </xsl:copy>
  </xsl:template>
  
  <xsl:template match="text()[parse-xml(.) instance of document-node()]">
    <xsl:variable name="cdata-doc" select="parse-xml(.)"/>
    <xsl:apply-templates select="$cdata-doc/*">
      <xsl:with-param name="path-prefix" tunnel="yes" select="path(..) || '/parse-xml(.)'"/>
    </xsl:apply-templates>
  </xsl:template>
  
  <xsl:mode on-no-match="shallow-copy"/>

  <xsl:output indent="yes"/>

</xsl:stylesheet>

Note that your CDATA, due to white space before the XML declaration, doesn't parse as XML, so for that example to work you need e.g.

  <xsl:template match="text()[parse-xml(normalize-space(.)) instance of document-node()]">
    <xsl:variable name="cdata-doc" select="parse-xml(normalize-space(.))"/>
    <xsl:apply-templates select="$cdata-doc/*">
      <xsl:with-param name="path-prefix" tunnel="yes" select="path(..) || '/parse-xml(normalize-space())'"/>
    </xsl:apply-templates>
  </xsl:template>

instead.

I have used the result of the path function to construct the xpath attribute value but you should be able to replace that with a custom function producing your wanted format.