Search code examples
xqueryxslt-2.0

What “Attribute node cannot follow non-attribute node in element content” tells me


one-attr.xml

<requestConfirmation xmlns="http://example/confirmation">
    <trade>
        <amount>
            <currency id="settlementCurrency">USD</currency>
            <referenceAmount>StandardISDA</referenceAmount>
            <cashSettlement>true</cashSettlement>
        </amount>
    </trade>
</requestConfirmation>

two-attr.xml

<requestConfirmation xmlns="http://example/confirmation">
    <trade>
        <cal>
          <c>PRECEDING</c>
            <bcs id="businessCenters">
              <bc>USNY</bc>
              <bc>GBLO</bc>
            </bcs>
        </cal>  
        <amount>
            <currency id="settlementCurrency" currencyScheme="http://example/iso4">USD</currency>
            <referenceAmount>StandardISDA</referenceAmount>
            <cashSettlement>true</cashSettlement>
        </amount>
    </trade>
</requestConfirmation>

I use XQuery to transform the id attribute into element. There are only two documents like two-attr.xml out of 70K documents. Apparently, the currency element already has value USD. I got below error in the ML QConsole when transforming two-attr.xml. I got very similar error in Oxygen.

XDMP-ATTRSEQ: (err:XQTY0024) $node/@*[fn:local-name(.) = $attr] -- Attribute node cannot follow non-attribute node in element content

My XQuery module:

declare namespace hof = "http://fc.fasset/function";
declare function hof:remove-attr-except
  ( $node as node()* ,
    $newNs as xs:string ,
    $keepAttr as xs:string* 
  ) as node()* 
  {
    for $attr in $node/@*
    return 
    if (local-name($attr) = $keepAttr)
    then (element {fn:QName ($newNs, name($attr))} {data($attr)})
    else
      $node/@*[name() = $keepAttr], hof:transform-ns-root-flatten($node/node(), $newNs, $keepAttr)
  };
declare function hof:transform-ns-root-flatten
  ( $nodes as node()* ,
    $newNs as xs:string ,
    $keepAttr as xs:string*
  ) as node()* 
  {
    for $node in $nodes
    return 
        typeswitch($node)
            case $node as element()
                return (element { fn:QName ($newNs, local-name($node)) }
                            { hof:remove-attr-except($node, $newNs, $keepAttr) }
                       )
            case $node as document-node()
                return hof:transform-ns-root-flatten($node/node(), $newNs, fn:normalize-space($keepAttr))          
    default return $node 
  };
(:  let $inXML := doc("/fasset/bug/two-attr.xml") :)
let $inXML := 
let $inXML := 
<requestConfirmation xmlns="http://example/confirmation">
    <trade>
      <cal>
         <c>PRECEDING</c>
            <bcs id="businessCenters">
              <bc>USNY</bc>
              <bc>GBLO</bc>
            </bcs>
        </cal>  
        <amount>
            <currency id="settlementCurrency" currencyScheme="http://example/iso4">USD</currency>
            <referenceAmount>StandardISDA</referenceAmount>
            <cashSettlement>true</cashSettlement>
        </amount>
    </trade>
</requestConfirmation>
let $input := $inXML/*[name() = name($inXML/*)]/* 
let $ns := "schema://fc.fasset/execution"
let $root := "executionReport"
let $keep := "id"
return 
element { fn:QName ($ns, $root)  }
        { hof:transform-ns-root-flatten($input, $ns, $keep) }

Then I switch XSLT to transform two-attr.xml. Surprisingly, the XSLT transform is a success.

    <xsl:param name="ns" as="xs:string">schema://fc.fasset/product</xsl:param>
    <xsl:param name="attr" static="yes" as="xs:string*" select="'href', 'id'"/>
    =================================
    <xsl:template match="@*">
        <xsl:choose>
            <xsl:when test="local-name() = $attr">
                <xsl:element name="{local-name()}" namespace="{$ns}">
                    <xsl:value-of select="."/>
                </xsl:element>
            </xsl:when>
        </xsl:choose>
    </xsl:template>

The collective successful underlying transform is against the one-attr.xml model. Java|ML API, Oxygen, XSLT returns the same result:

            <amount>
               <currency>
                  <id>settlementCurrency</id>USD</currency>
               <referenceAmount>StandardISDA</referenceAmount>
               <cashSettlement>true</cashSettlement>
            </amount>

Now here is the rub: it doesn’t look like a valid XML. For although I can get the currency text value

doc("/product/eqd/a7c1db2d.xml")//prod:trade//prod:amount/prod:currency/text()

, I expect below result to facilitate the search engine:

<executionReport xmlns="schema://fc.fasset/execution">
    <trade>
    <cal>
        <c>PRECEDING</c>
        <bcs>
            <id>businessCenters</id>
            <bc>USNY</bc>
            <bc>GBLO</bc>
        </bcs>
    </cal>
    <amount>
        <currency>USD</currency>        
        <id>settlementCurrency</id>
        <referenceAmount>StandardISDA</referenceAmount>
        <cashSettlement>true</cashSettlement>
    </amount>
    </trade>
</executionReport>

Among the following solutions, the latest result is as below:

<executionReport xmlns="schema://fc.fasset/execution">
    <cal>
        <c>PRECEDING</c>
        <bcs>
            <bc>USNY</bc>
            <bc>GBLO</bc>
        </bcs>
<!-- Line9: id is out of <bcs> element and its context is completed lost!  -->
        <id>businessCenters</id>
    </cal>
    <amount>
        <currency>USD</currency>
<!-- Line14: id is in the correct position! -->
        <id>settlementCurrency</id>
        <referenceAmount>StandardISDA</referenceAmount>
        <cashSettlement>true</cashSettlement>
    </amount>
</executionReport>

How can I get my XQuery and XSLT module work?


Solution

  • You can't create attributes after you have started creating child nodes. So, if you are transforming the @id into <id> then you have to do that AFTER you have copied the other attributes.

    The shortest and easiest way to avoid the problem is to sort the attributes, ensuring that the ones that will be copied forward are processed first, then the ones that will be converted to elements.

    You could achieve that by sorting the sequence of items returned from the hof:remove-attr-except() function, ensuring that the sequence has attributes and then the elements:

    element { fn:QName ($newNs, local-name($node)) }
      { for $item in (hof:remove-attr-except($node, $newNs, $keepAttr))
        order by $item instance of attribute() descending
        return $item }
    

    You could also just have two separate FLWOR with a where clause that processes the $keepAttr and then those that will be converted into elements:

    declare function hof:remove-attr-except
      ( $node as node()* ,
        $newNs as xs:string ,
        $keepAttr as xs:string* 
      ) as node()* 
      {
        for $attr in $node/@*
        where not(local-name($attr) = $keepAttr)
        return
          $node/@*[name() = $keepAttr], hof:transform-ns-root-flatten($node/node(), $newNs, $keepAttr)
        ,  
        for $attr in $node/@*
        where local-name($attr) = $keepAttr
        return 
         element {fn:QName ($newNs, name($attr))} {data($attr)}    
      };
    

    But if you want those new elements to be outside of the original element, and you don't want to retain the attributes then I would change the processing of the element in your typeswitch, so that you call the function that converts those attributes into elements outside of the element constructor:

    declare namespace hof = "http://fc.fasset/function";
    
    declare function hof:attr-to-element
      ( $node as node()* ,
        $newNs as xs:string ,
        $keepAttr as xs:string* 
      ) as node()* 
      {  
        for $attr in $node/@*
        where local-name($attr) = $keepAttr
        return 
         element {fn:QName ($newNs, name($attr))} {data($attr)}    
      };  
      
    declare function hof:transform-ns-root-flatten
      ( $nodes as node()* ,
        $newNs as xs:string ,
        $keepAttr as xs:string*
      ) as node()* 
      {
        for $node in $nodes
        return 
            typeswitch($node)
                case $node as element()
                    return (element { fn:QName ($newNs, local-name($node)) }
                                { hof:transform-ns-root-flatten($node/node(), $newNs, $keepAttr)  }
                            ,
                            hof:attr-to-element($node, $newNs, $keepAttr)
                           )
                case $node as document-node()
                    return hof:transform-ns-root-flatten($node/node(), $newNs, fn:normalize-space($keepAttr))          
        default return $node 
      };
    

    The code above produces the following output from the provided input XML:

    <executionReport xmlns="schema://fc.fasset/execution">
      <amount>
        <currency>USD</currency>
        <id>settlementCurrency</id>
        <referenceAmount>StandardISDA</referenceAmount>
        <cashSettlement>true</cashSettlement>
      </amount>
    </executionReport>