Showing posts with label XSL. Show all posts
Showing posts with label XSL. Show all posts

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.

Saturday, July 4, 2009

Saxon, Command Line, C#, and XSL 2.0

I've been using Xalan/Xerces for command line XSL transformations for years, but I've been moving farther away from Java over the years, so I wanted something .NET compatible and I wanted something XSL 2.0 compatible. I finally switched to Saxon.

I normally use the standard XML objects in my ASP.NET apps, but I'll switch to Xalan command line tools when I need the "write" extension. I can do the same with Saxon now.

C:\Program Files\Saxon.NET>bin\Transform SaxonTest.xml SaxonTest.xsl


Here is the C# code to call an XSL transformation using Saxon. This one may seem a little odd because the code doesn't save any file since what I'm doing is splitting a large XML file into multiple small files using <xsl:result-document>.

// Create a Processor instance.
Processor p = new Processor();
// Load the source document.
XdmNode node = p.NewDocumentBuilder().Build(new Uri(file));
// Create a transformer for the stylesheet.
XsltTransformer transformer = p.NewXsltCompiler().Compile(myStream).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.
transformer.BaseOutputUri = new Uri(file);
// Create a serializer.
Serializer serializer = new Serializer();
transformer.Run(serializer);


Here is the stylesheet I used to leverage the XSL 2.0 equivalent of xalan:write.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fo="http://www.w3.org/1999/XSL/Format"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:fn="http://www.w3.org/2005/xpath-functions"
exclude-result-prefixes="fo xs fn">

<xsl:output method="xml" indent="yes" encoding="UTF-8" name="xmlFormat"/>

<xsl:template match="text()" />

<xsl:template match="/">
<xsl:for-each select="//node()[@fragment='true']">
<xsl:variable name="filename" select="concat( /gpg-book/@local-id, '/', @local-id, '.xml' )"/>
<xsl:result-document href="{$filename}" format="xmlFormat">
<pcu-gpg-book>
<xsl:copy-of select="/gpg-book/taxonomy_pcu"/>
<xsl:copy-of select="/gpg-book/content-metadata"/>
<xsl:copy-of select="/gpg-book/print-pub-metadata"/>
<xsl:copy-of select="parent::node()"/>
</pcu-gpg-book>
</xsl:result-document>
</xsl:for-each>
</xsl:template>

</xsl:stylesheet>

Thursday, May 7, 2009

Getting Unique XML Element Values with XSL 1.0

Today I needed to munge some dirty XML data. I still haven't taught myself XSL/XPath 2.0 yet, so I was limited to XSL 1.0 for now. The data I had looked like this, only much, much worse.
<Subject>Value 1|Value 2</Subject>
<Subject>Value 1|Value 2</Subject>
<Subject>Value 1|Value 2</Subject>
<Time>Time Value 1</Time>
<Time>Time Value 1</Time>
<Time>Time Value 1</Time>
<Subject>Value 3|Value 4</Subject>
<Subject>Value 3|Value 4</Subject>
<Subject>Value 3|Value 4</Subject>
<Time>Time Value 2</Time>
<Time>Time Value 2</Time>
<Time>Time Value 2</Time>
I wanted two things out of that series of elements: unique strings and the value before the |. As I type this, I realize there may be a bit of a bug here, but I'll have to test it out. Here's what I did for the series of subject elements. Can you spot the bug? ;-)
<xsl:variable name="subjects" select="/fragment/index-only-subjects//Subject[not(text()=preceding-sibling::Subject/text())]/text()"/>
<xsl:for-each select="$subjects">
<xsl:sort select="." data-type="text"/>
<subject>
<xsl:choose>
<xsl:when test="contains(.,'|')">
<xsl:value-of select="substring-before(.,'|')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="."/>
</xsl:otherwise>
</xsl:choose>
</subject>
</xsl:for-each>
I'm pretty sure there's a way to do this with xsl:keys / key(), but I got this solution working first.

Tuesday, December 11, 2007

Odd and Even Numbers in XSL 1.0

Here's a way to tell if a number is odd or even in XSL 1.0, in this case as part of a snippet of code to switch CSS styles between rows in a search results set.
<xsl:attribute name="class">
<xsl:choose>
<!--returns true for odd counts-->
<xsl:when test="$number mod 2 = 1">highlight</xsl:when>
<!--returns true for even counts-->
<!--<xsl:when test="$number mod 2 = 0">nohighlight</xsl:when>-->
<xsl:otherwise>nohighlight</xsl:otherwise>
</xsl:choose>
</xsl:attribute>

Wednesday, June 6, 2007

Apache Xalan from the Command Line

Check your Xalan environment:

C:\>java org.apache.xalan.xslt.EnvironmentCheck

Set your environment for the proper JARs (check the Apache site for updates):
C:\>set classpath=.;C:\jdk1.2.2\jre\lib\ext\xalan.jar;
C:\jdk1.2.2\jre\lib\ext\xercesImpl.jar;
C:\jdk1.2.2\jre\lib\ext\xml-apis.jar
Run a basic XSL transformation:

C:\>java org.apache.xalan.xslt.Process -IN c:\tmp\intput.xml -XSL c:\tmp\transform.xsl -OUT c:\tmp\output.xml

Additional command line options:

Common Options

-XSLTC (use XSLTC for transformation)
-IN inputXMLURL
-XSL XSLTransformationURL
-OUT outputFileName
-V (Version info)
-EDUMP [optional filename] (Do stackdump on error.)
-XML (Use XML formatter and add XML header.)
-TEXT (Use simple Text formatter.)
-HTML (Use HTML formatter.)
-PARAM name expression (Set a stylesheet parameter)
-MEDIA mediaType (use media attribute to find stylesheet associated with a document)
-FLAVOR flavorName (Explicitly use s2s=SAX or d2d=DOM to do transform)
-DIAG (Print overall milliseconds transform took)
-URIRESOLVER full class name (URIResolver to be used to resolve URIs)
-ENTITYRESOLVER full class name (EntityResolver to be used to resolve entities)
-CONTENTHANDLER full class name (ContentHandler to be used to serialize output)

Options for Xalan-Java Interpretive

-QC (Quiet Pattern Conflicts Warnings)
-TT (Trace the templates as they are being called)
-TG (Trace each generation event)
-TS (Trace each selection event)
-TTC (Trace the template children as they are being processed)
-TCLASS (TraceListener class for trace extensions)
-L (use line numbers for source document)
-INCREMENTAL (request incremental DTM construction by setting
http://xml.apache.org/xalan/features/incremental to true)
-NOOPTIMIMIZE (request no stylesheet optimization proccessing by setting
http://xml.apache.org/xalan/features/optimize to false)
-RL recursionlimit (assert numeric limit on stylesheet recursion depth)

For more information see http://xml.apache.org/xalan-j/commandline.html.

Friday, February 9, 2007

Transforming XML with XSL: Method 1

There are several ways to transform an XML document with XSL. This is one approach that includes passing parameters to the XSL and assumes the transformation is targeted for a <div> element. You'll need the following namespaces added, assuming this is an ASP.NET website: System.IO, System.Xml.Xsl, System.Xml.XPath. This should be wrapped in a try/catch block.
XPathDocument xpathDocument = new XPathDocument(this.Server.MapPath("/xml/test.xml"));
XslCompiledTransform xpathTransform = new XslCompiledTransform();
StringWriter xpathWriter = new StringWriter();
XsltArgumentList xslArg = new XsltArgumentList();
xpathTransform.Load(this.Server.MapPath("/xsl/toText.xsl"));
xslArg.AddParam("path", "", imgPath); /* imgPath defined elsewhere */
xslArg.AddParam("country", "", country); /* country defined elsewhere */
xpathTransform.Transform(xpathDocument, xslArg, xpathWriter);
divXml.InnerHtml = xpathWriter.ToString(); /* divXml is a control on the ASPX page */


UPDATE: Why would you need this approach versus using the Xml control? I ran into a situation where some legacy XSL used the document() function and called a file on the network. I needed to 1) use impersonation and 2) set XmlSetting when I called Load().