Project

Profile

Help

Bug #5750

closed

SaxonCS-b6 11.4.1 unable to set XsltTransformer.InitialContextNode - its always null after setting it!

Added by Nadeem Mirza 2 months ago. Updated 21 days ago.

Status:
Resolved
Priority:
Normal
Assignee:
-
Category:
.NET API
Sprint/Milestone:
-
Start date:
2022-11-29
Due date:
% Done:

100%

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

Description

I am using SaxonCS-b6 11.4.1 for .NET 6.0 and having problems trying to set XsltTransformer.InitialContextNode

doc = builder.Build(reader); XsltTransformer.InitialContextNode = doc; // This is not setting the InitialContextNode property its always null.

Can anyone help?


Files

clipboard-202211301451-xlez9.png (70.4 KB) clipboard-202211301451-xlez9.png Nadeem Mirza, 2022-11-30 15:51
Actions #1

Updated by Michael Kay 2 months ago

I can't see anything obviously wrong with the code, but I've noticed that most of the Java unit tests for XsltTransformer haven't been ported across to C#, so my first task is to port them and run them.

What are the symptoms of the failure? Is it just that accessing the property after setting it returns null, or is it that setting the property has no effect on the subsequent transformation?

The documentation of this property could be improved, in particular the way it interacts with other properties. It would be good to have a repro that shows exactly what you are doing so we can add this as an explicit test case.

Actions #2

Updated by Martin Honnen 2 months ago

Mike, as far as I have tested, yes, the getter of the property on the CS side returns null even if you have set the property before to an XdmNode. A transformation seems to use the set InitialContextNode just fine, only somehow the setter of the property returns null.

So the following outputs True:

using Saxon.Api;

Processor processor = new();

var docBuilder = processor.NewDocumentBuilder();

docBuilder.BaseUri = new Uri("urn:from-string");

var xml = "<root><item>b</item><item>a</item><item>c</item></root>";

var xdmDoc = docBuilder.Build(new StringReader(xml));

var xslt = "<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform' version='3.0'><xsl:variable name='node-count' select='count(//*)'/><xsl:mode on-no-match='shallow-copy'/><xsl:template match='/'><xsl:next-match/><xsl:comment expand-text='yes'>Count: {$node-count}</xsl:comment></xsl:template></xsl:stylesheet>";

var xsltTransformer = processor.NewXsltCompiler().Compile(new StringReader(xslt)).Load();

xsltTransformer.InitialContextNode = xdmDoc;

Console.WriteLine(xsltTransformer.InitialContextNode == null);

If I add xsltTransformer.Run(processor.NewSerializer(Console.Out)); that does its job to run the transformation fine, but the setter is broken (or wrongly documented).

Actions #3

Updated by Martin Honnen 2 months ago

"but the setter is broken (or wrongly documented)." was supposed to say "but the getter is broken (or wrongly documented)."

Actions #4

Updated by Michael Kay 2 months ago

Firstly, the documentation of the property is incomplete. It's implemented as a shim over the Java code XsltTransformer.setInitialContextNode(), documented here: https://www.saxonica.com/documentation11/index.html#!javadoc/net.sf.saxon.s9api/XsltTransformer@setInitialContextNode

which hints at the tangled mess of the JAXP API, sadly endorsed by the XSLT 2.0 specification, whereby setting a source node for the transformation causes the initial template rule to be the one that matches the supplied node, but the global context item ("." within global variables) to be the root node of that document. One of the reasons for introducing the Xslt30Transformer was to extricate ourselves from that mess.

In the test I'm running, I'm setting the context item to the outermost element E of a document D. In the Java code, this sets two variables: initialSource is set to (the NodeInfo underlying) E, and controller.setGlobalContextItem() is set to D. This runs the transformation successfully applying the strange JAXP-derived rules.

As Martin suggests, the problem seems to be with the getter for the property. I'm having problems with the debugger seeing what's going on there.

Actions #5

Updated by Nadeem Mirza 2 months ago

If the InitialContextNode is actually being set on my XslTransformer object, but the getter showing null, then the issue I'm having maybe elsewhere. Let me explain, previously our application was targeting .NET472 and we were using Saxon-HE 9.7.0.13. I am migrating our application to target .NET6. This is why we have to use SaxonCS-B6 11.4.1. I have refactored our code, but following on from your response, I think the problem maybe to do with the XmlResolver. Either its not being set in the XmlReaderSettings or the GetEntity method we have overridden in our CreateXmlUrlResolver() is not being fired. OR while migrating our code to target .NET 6, I'm doing something wrong. Any help to find the root cause and resolve this would be appreciated. Thanks.

BEFORE

Previously in our old .NET472 app using Saxon-HE 9.7.0.13 we had soemthing like this:

using (var reader = document.Value.CreateReader())
. // other initialisation code here...
var transformer = _stylesheetLoader(context).Load();
.
transformer.InputXmlResolver = _options.XmlResolver ?? context.CreateXmlResolver();
. // other code here...
var builder = _processor.NewDocumentBuilder();
var doc = builder.Build(reader);

transformer.InitialContextNode = doc;

var serializer = new Serializer();
serializer.SetOutputWriter(writer);

transformer.Run(serializer);
serializer.Close();
writer.Flush();
content = buffer.ToArray();

AFTER

var xmlReaderSettings = new XmlReaderSettings
{
	CheckCharacters = false,
	XmlResolver = _options.XmlUrlResolver == null ? context.CreateXmlUrlResolver() : (XmlResolver)_options.XmlUrlResolver
};
.
using (var reader = XmlReader.Create(document.Value.CreateReader(), xmlReaderSettings))
.
.
var transformer = _stylesheetLoader(context).Load();
.
var builder = _processor.NewDocumentBuilder();
var doc = builder.Build(reader);

transformer.InitialContextNode = doc; // << this is the bug I raised, but you mentioned its actually being set. Getter is showing null, which needs resolved.

var serializer = _processor.NewSerializer(); // previously: var serializer = new Serializer();
serializer.OutputWriter = writer;            // previously: serializer.SetOutputWriter(writer);

transformer.Run(serializer);   // *** My code actually errors here. I'm thinking the XmlResolver is either not being set or override GetEntity method not triggered. ***
serializer.Close();
writer.Flush();

content = buffer.ToArray();

Actions #6

Updated by Martin Honnen 2 months ago

How does your code error exactly? Can you add the exact exception/stack trace?

Also what kind of API do you use/what kind of XmlReader does document.Value.CreateReader() create, is that an XmlReader over a DOM (System.Xml.XmlNode) or Linq (System.Xml.Linq.XNode)?

Actions #7

Updated by Michael Kay 2 months ago

The underlying Java code is incorrect (I extended the Java unit tests and got a failure). The Java code for XsltTransformer.getInitialContextNode() is returning null if the value of initialSource is not a NodeInfo: it is actually a NodeSource which wraps a NodeInfo. That one is easily fixed.

Actions #8

Updated by Nadeem Mirza 2 months ago

Martin Honnen wrote in #note-6:

How does your code error exactly? Can you add the exact exception/stack trace?

exception/stack trace as follows:

Also what kind of API do you use/what kind of XmlReader does document.Value.CreateReader() create, is that an XmlReader over a DOM (System.Xml.XmlNode) or Linq (System.Xml.Linq.XNode)?

Drilling down the document.Value.CreateReader() creates a System.Xml.Linq.XNode

Actions #9

Updated by Michael Kay 2 months ago

In your code in comment #5, the XmlResolver is passed directly to the (Microsoft) XML parser, and is completely unknown to Saxon, so if the problem is in that area, then it's difficult to help you with it.

In the 9.7 code your XmlResolver was being used any time Saxon invoked the XML parser, for example on calls of doc() or document(), and that's no longer the case with the 11.x code, where it is used only for the principal source document. If the XmlResolver is needed for secondary input documents, then you need a different approach.

I think you may have to switch from the XsltTransformer to the Xslt30Transformer, which has an XmlDocumentResolver property. Its value is a ResourceResolver, which is a generalisation of Java's URIResolver and .NET's XmlUrlResolver designed to resolve a wider class of resources including for example JSON files. Alternatively if you stick with the XsltTransformer you can set a ResourceResolver at the Processor level, but it will then be called for everything.

Actions #10

Updated by Michael Kay 2 months ago

Yes, your stack trace suggests you probably need to configure a ResourceResolver at the Saxon level, not just the XML parser level.

Actions #11

Updated by Nadeem Mirza 2 months ago

Michael Kay wrote in #note-10:

Yes, your stack trace suggests you probably need to configure a ResourceResolver at the Saxon level, not just the XML parser level.

Hi all, I'm still having problems migrating my 9.7 code (targeting .NET 472) to 11.4.1 (targeting .NET6). I was trying to cast the XmlResolver to a ResourceResolver. That's not really working if I stick with XsltTransformer. Changing to XsltTransformer30 then that doesn't have .InitialContextNode property or .Run() method. I'm a bit stuck!

To be honest, I have inherited this code and would appreciate any help in resolving. Do you have any examples on your documentation or forum, that explains migrating to 11.x from 9.7? I've looked through the SaxonCS Examples.cs but very little on ResourceResolver.

As previously mentioned, our original code has a class DocumentXmlUrlResolver that implements XmlUrlResolver. We pass a custom context into the constructor. Then we also override the GetEntity(Uri absoluteUri, string role, Type ofObjectToReturn) method, where we use our custom context to do the work. I would need something similar, but ResourceResolver does not implement XmlUrlResolver, and I'm therefore unable to override GetEntity(),

I realise our code needs to be refactored to use the 11.4 SaxonCS library, but I wanted to make the minimum changes to resolve. Any further help would be appreciated. Thanks in advance.

Actions #12

Updated by Michael Kay 2 months ago

I will retrofit the XmlDocumentResolver property from Xslt30Transformer to XsltTransformer, but that doesn't help you in the short term.

I imagine you're running XSLT in the classic way by applying templates to a source document, so to switch from XsltTransformer to Xslt30Transformer, set the GlobalContextItem property to the root node of the source document, and call ApplyTemplates(doc, serializer).

As for the resolver, it depends a little on exactly what calls your XmlUrlResolver is expecting and what it returns from its GetEntity() method. One reason we abandoned this interface is that it's very badly documented, and the Microsoft XML parser calls it in very weird ways, for example supplying a Public Identifier instead of a URL. But typically you'll probably be expecting a base URI and relative URI, and be returning a Stream, and you should be able to do something like

transformer.XmlDocumentResolver = (request) => { Uri abs = myResolver.Resolve(request.BaseUri, request.RelativeUri); Stream input = myResolver.GetEntity(abs, ....); return new NodeResource(builder.Build(stream)); }

Alternatively you could return an XmlReaderResource wrapping a DocumentBuilder and a System.Xml.XmlReader, with the latter initialized to use your XmlUrlResolver.

Actions #13

Updated by Nadeem Mirza about 2 months ago

Michael Kay wrote in #note-12:

I will retrofit the XmlDocumentResolver property from Xslt30Transformer to XsltTransformer, but that doesn't help you in the short term.

I imagine you're running XSLT in the classic way by applying templates to a source document, so to switch from XsltTransformer to Xslt30Transformer, set the GlobalContextItem property to the root node of the source document, and call ApplyTemplates(doc, serializer).

As for the resolver, it depends a little on exactly what calls your XmlUrlResolver is expecting and what it returns from its GetEntity() method. One reason we abandoned this interface is that it's very badly documented, and the Microsoft XML parser calls it in very weird ways, for example supplying a Public Identifier instead of a URL. But typically you'll probably be expecting a base URI and relative URI, and be returning a Stream, and you should be able to do something like

transformer.XmlDocumentResolver = (request) => { Uri abs = myResolver.Resolve(request.BaseUri, request.RelativeUri); Stream input = myResolver.GetEntity(abs, ....); return new NodeResource(builder.Build(stream)); }

Alternatively you could return an XmlReaderResource wrapping a DocumentBuilder and a System.Xml.XmlReader, with the latter initialized to use your XmlUrlResolver.

Yes, seems to be classic way. I have refactored to use Xslt30Transformer, with GlobalContextItem and ApplyTemplates. That seems to work fine. Now I need to resolve this

Michael Kay wrote in #note-12:

I will retrofit the XmlDocumentResolver property from Xslt30Transformer to XsltTransformer, but that doesn't help you in the short term.

I imagine you're running XSLT in the classic way by applying templates to a source document, so to switch from XsltTransformer to Xslt30Transformer, set the GlobalContextItem property to the root node of the source document, and call ApplyTemplates(doc, serializer).

As for the resolver, it depends a little on exactly what calls your XmlUrlResolver is expecting and what it returns from its GetEntity() method. One reason we abandoned this interface is that it's very badly documented, and the Microsoft XML parser calls it in very weird ways, for example supplying a Public Identifier instead of a URL. But typically you'll probably be expecting a base URI and relative URI, and be returning a Stream, and you should be able to do something like

transformer.XmlDocumentResolver = (request) => { Uri abs = myResolver.Resolve(request.BaseUri, request.RelativeUri); Stream input = myResolver.GetEntity(abs, ....); return new NodeResource(builder.Build(stream)); }

Alternatively you could return an XmlReaderResource wrapping a DocumentBuilder and a System.Xml.XmlReader, with the latter initialized to use your XmlUrlResolver.

Thank you so much for all your help and support. I have changed to use XsltTransformer30, set the GlobalContextItem and call ApplyTemplates as suggested.

I also used your code snippet and refactored my code to:

                    var inputXmlResolver = _options.XmlUrlResolver ?? context.CreateXmlUrlResolver();

                    transformer.XmlDocumentResolver = (request) =>
                    {
                        Uri abs = inputXmlResolver.ResolveUri(request.BaseUri, request.RelativeUri);
                        Stream input = (Stream)inputXmlResolver.GetEntity(absoluteUri: abs, role: null, ofObjectToReturn: typeof(Stream));
                        return new NodeResource(builder.Build(input));
                    };

Our application is now working as expected and targeting .NET6. There's a lot more regression testing to perform but I think it's looking great! Thanks again and you may now close this bug report.

Actions #14

Updated by Michael Kay about 2 months ago

  • Status changed from New to Resolved
  • Priority changed from High to Normal
  • Applies to branch trunk added
  • Fix Committed on Branch 11, trunk added

Added resolver support to XsltTransformer class.

Actions #15

Updated by Community Admin 21 days ago

  • % Done changed from 0 to 100
  • Fixed in Maintenance Release 12.0 added

Bug issue fix applied in the Saxon 12.0 Major Release. Leaving this bug marked as Resolved until fix applied

Please register to edit this issue

Also available in: Atom PDF