Project

Profile

Help

Bug #6626

closed

When chaining two streamable stylesheets with asDocumentDestination, an accumulator isn't copied to grounded nodes created with copy-of()

Added by Martin Honnen 15 days ago. Updated 5 days ago.

Status:
Resolved
Priority:
Normal
Assignee:
-
Category:
s9api API
Sprint/Milestone:
Start date:
2025-01-06
Due date:
% Done:

0%

Estimated time:
Legacy ID:
Applies to branch:
12, trunk
Fix Committed on Branch:
12, trunk
Fixed in Maintenance Release:
Platforms:
.NET, Java

Description

Testing with Saxon 12.5 EE Java, I have found a strange problem, I have an accumulator in a streamable stylesheet that is also then used for a "grounded" element node created with copy-of(); when I run the stylesheet stand-alone the grounded element node has the accumulator value, however, when I chain two streamable stylesheets with asDocumentDestination it appears as if the accumulator (value) is not copied to the element node created with copy-of().

Example XML;

<root>
    <foo>foo 1</foo>
    <bar>bar 1</bar>
</root>

Example XSLT with the accumulator:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:xs="http://www.w3.org/2001/XMLSchema"
                xmlns:saxon="http://saxon.sf.net/"
                exclude-result-prefixes="#all"
                version="3.0"
                expand-text="yes">

    <xsl:accumulator name="latest-foo-value" streamable="yes" initial-value="()" as="xs:string?">
        <xsl:accumulator-rule match="foo/text()" select="string()"/>
    </xsl:accumulator>

    <xsl:mode on-no-match="shallow-copy" streamable="yes" use-accumulators="latest-foo-value"/>

    <xsl:mode name="grounded" on-no-match="shallow-copy"/>

    <xsl:template match="bar">
        <xsl:comment>node {node-name()}: accumulator-before('latest-foo-value'): {accumulator-before('latest-foo-value')}</xsl:comment>
        <xsl:apply-templates select="copy-of()" mode="grounded"/>
    </xsl:template>

    <xsl:template mode="grounded" match="bar">
        <xsl:comment>node {node-name()}: accumulator-before('latest-foo-value'): {accumulator-before('latest-foo-value')}</xsl:comment>
        <xsl:next-match/>
    </xsl:template>

    <xsl:template match="/">
        <xsl:copy>
            <xsl:apply-templates/>
            <xsl:comment>Run with {static-base-uri()} at {current-dateTime()}</xsl:comment>
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>

Correct result when running that stylesheet stand-alone against the input sample:

<?xml version="1.0" encoding="UTF-8"?><root>
    <foo>foo 1</foo>
    <!--node bar: accumulator-before('latest-foo-value'): foo 1--><!--node bar: accumulator-before('latest-foo-value'): foo 1--><bar>bar 1</bar>
</root><!--Run with file:/C:/Users/marti/IdeaProjects/SerializerProblem2/./accumulator-test1.xsl at 2025-01-06T11:45:44.7716048+01:00-->

Result when chaining with a streamed identity transformation

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:xs="http://www.w3.org/2001/XMLSchema"
                xmlns:saxon="http://saxon.sf.net/"
                exclude-result-prefixes="#all"
                version="3.0"
                expand-text="yes">

    <xsl:mode on-no-match="shallow-copy" streamable="yes"/>

    <xsl:template match="/">
        <xsl:copy>
            <xsl:apply-templates/>
            <xsl:comment>Run with {static-base-uri()} at {current-dateTime()}</xsl:comment>
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>

and the Java code

package org.example;

import net.sf.saxon.s9api.*;

import javax.xml.transform.stream.StreamSource;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Main {
    public static void main(String[] args) throws SaxonApiException, IOException {

        Processor processor = new Processor(true);

        XsltCompiler xsltCompiler = processor.newXsltCompiler();

        XsltExecutable xsltExecutable1 = xsltCompiler.compile(new File("identity-transformation.xsl"));

        Xslt30Transformer xslt30Transformer1 = xsltExecutable1.load30();

        XsltExecutable xsltExecutable2 = xsltCompiler.compile(new File("accumulator-test1.xsl"));

        Xslt30Transformer xslt30Transformer2 = xsltExecutable2.load30();

        xslt30Transformer1.applyTemplates(
                new StreamSource(new File("sample1.xml")),
                xslt30Transformer2.asDocumentDestination(xslt30Transformer2.newSerializer(Files.newOutputStream(Paths.get("result3.xml"))))
        );
    }
}

is

<?xml version="1.0" encoding="UTF-8"?><root>
    <foo>foo 1</foo>
    <!--node bar: accumulator-before('latest-foo-value'): foo 1--><!--node bar: accumulator-before('latest-foo-value'): --><bar>bar 1</bar>
</root><!--Run with file:/C:/Users/marti/IdeaProjects/SerializerProblem2/identity-transformation.xsl at 2025-01-06T11:42:17.241+01:00--><!--Run with file:/C:/Users/marti/IdeaProjects/SerializerProblem2/accumulator-test1.xsl at 2025-01-06T11:42:17.242+01:00-->

so here it seems the accumulator (value) is not copied to the bar element created with copy-of().

Actions #1

Updated by Michael Kay 7 days ago

I've reproduced the problem but it's a pretty tough debugging challenge.

I've established that the copy-of() instruction appears to be copying the accumulator value OK, but the call on accumulator-before() isn't picking up the value for some reason.

In CopyOfFeed.close(), the Controller obtained using watchManager.getPipelineConfiguration().getController() is not the same as the Controller obtained using context.getController() in AccumulatorFn.getAccumulatorValue().

Actions #2

Updated by Michael Kay 7 days ago

Changing CopyOfFeed.close() to get the Controller via WatchManager.getContext().getController() solves the problem. Not regression tested. It feels somewhat fragile though. Should both transformations be using the same PipelineConfiguration? What are the consequences of changing this?

There's only one other place that uses WatchManager.getContext(), whereas WatchManager.getPipelineConfiguration() is called from all over the place.

The PipelineConfiguration of the WatchManager of the first transformation is initially set up correctly, but the subsequent call of applyTemplates() on the first Transformer triggers a call of AbstractXsltTransformer.getDestinationReceiver(), which creates a new PipelineConfiguration whose Controller is that for the second transformation, and the subsequent calls of setPipelineConfiguration propagate, eventually hitting the WatchManager, which will now be pointing to the wrong Controller. The unfortunate implication is that getting the Controller from the PipelineConfiguration is unsafe.

There are 18 places that call PipelineConfiguration.getController(). mostly concerned either with schema processing or serialization.

Actions #3

Updated by Michael Kay 5 days ago

Fixed the problem by the rather unsatisfactory kludge of ensuring that WatchManager.setPipelineConfiguration() ignores the supplied pipelineConfiguration if this would change the Controller in use.

Actions #4

Updated by Michael Kay 5 days ago

  • Status changed from New to Resolved
  • Applies to branch trunk added
  • Fix Committed on Branch 12, trunk added
  • Platforms .NET, Java added

Please register to edit this issue

Also available in: Atom PDF