Thursday, November 18, 2010

XSL Embedded Resources, xsl:include, Saxon HE, and You

I wanted to:
  1. Set my XSL files in my .NET console application to be embedded resources so they wouldn't be so easy to tinker with
  2. Use xsl:include to modularize some XSL where appropriate
  3. Pass a parameter into one of the XSL files
  4. Use Saxon for its XSL 2.0 support
At first, before I needed xsl:include, the code worked absolutely fine. The problem I ran into is apparently related to where the .NET framework was looking for the included XSL file when both were embedded resources. Step 1 to fixing the problem is implementing your own XmlUrlResolver. Step 2 is figuring out how to then reference your XSL from the calling code AND in the xsl:include href attribute.

I won't pretend to understand the details of what's going on here. My understanding is general, but this does work.

Here is where I ended up with my implementation of an XmlUrlResolver, including the sources I used.

public class EmbeddedXslResolver : XmlUrlResolver
{
private readonly Assembly _assembly;

public EmbeddedXslResolver()
{
_assembly = Assembly.GetExecutingAssembly();
}

public override object GetEntity(Uri absoluteUri, string role, Type ofObjectToReturn)
{
// Based on
//http://www.tkachenko.com/blog/archives/000653.html
//http://www.brandonmartinez.com/2009/07/06/xmlurlresolver-using-embedded-xslt-resources-in-c/

// Seems to assume content, not embedded resource
//Stream s = _assembly.GetManifestResourceStream(this.GetType(), Path.GetFileName(absoluteUri.AbsolutePath));

// Assumes input that would be only the file name like PrepGpgDtd.xsl and hard-codes the assembly details
//Stream s = _assembly.GetManifestResourceStream("Infrastructure.Xsl." + absoluteUri.Segments[absoluteUri.Segments.Length - 1]);

// Assumes input that would be fully qualified resource name like Infrastructure.Xsl.PrepGpgDtd.xsl
//Stream s = _assembly.GetManifestResourceStream(absoluteUri.Segments[absoluteUri.Segments.Length - 1]);

return _assembly.GetManifestResourceStream(absoluteUri.Segments[absoluteUri.Segments.Length - 1]);
}
}

Here's how I used it in my calling code. Note the string for my XSL file, "Infrastructure.Xsl.SplitDtd.xsl". My XSL was in an assembly (a C# class library project) named Infrastructure, in the Xsl folder, and the file was named SplitDtd.xsl. Under its properties, that file was set to "Embedded Resource" and "Do Not Copy".

...
EmbeddedXslResolver resolver = new EmbeddedXslResolver();
XmlReaderSettings settings = new XmlReaderSettings();
settings.XmlResolver = resolver;

using (XmlReader reader = XmlReader.Create("Infrastructure.Xsl.SplitGpgDtd.xsl", settings))
{
// Here are the Saxon details.
// Create a Processor instance.
Processor p = new Processor();

// Load the source document.
XdmNode node = p.NewDocumentBuilder().Build(new Uri(preparedXmlFile.FullName));

// Create a transformer for the stylesheet. Saxon needs the resolver, too.
XsltCompiler compiler = p.NewXsltCompiler();
compiler.XmlResolver = resolver;
XsltTransformer transformer = compiler.Compile(reader).Load();

// Set the root node of the source document to be the initial context node.
transformer.InitialContextNode = node;

// BaseOutputUri is only necessary for xsl:result-document, which I'm using.
transformer.BaseOutputUri = new Uri(destination.FullName);

transformer.SetParameter(new QName("", "", "a-head"), new XdmAtomicValue(splitOnAHead.ToString().ToLower()));

// Create a serializer.
Serializer serializer = null;
try
{
serializer = new Serializer();

// Transform the source XML to System.out.
transformer.Run(serializer);
}
finally
{
if(serializer != null) serializer.Close();
}
}

Then in SplitDtd.xsl, the xsl:include file was set to the following. Note how it's path looks incorrect in terms of a physical URI



If you're seeing an odd closing xsl:include tag, that seems to be a bug in the syntax highlighting.

With this configuration, .NET can find the primary XSL file and the include correctly.

One Saxon-specific note: I originally tried to use xsl:variable to catch the incoming parameter, but that doesn't work. You'll need to use xsl:param.

The information here is scattered around the Internet. I'm presenting nothing new. But, I didn't find this all in one place and I had a difficult time putting all the pieces together. Hopefully this post can prevent that in the future.

No comments: