How to resolve XSL includes in a Transformation that loads XSL from a String?
Asked Answered
E

4

7

.NET 2.0/VS2005

I am trying to use the XslCompiledTransform class to perform a XSL Transformation. I have two XSL files, the first of which includes a reference to the other in the form of an <xsl:include> statement :

Main.xsl:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:include href="Included.xsl" />
  ...
  ...
</xsl:stylesheet>

Now, If I could load the "Main.xsl" file itself as a URI, my transformation code would be as simple as :

// This is a function that works. For demo only.
private string Transform(string xslFileURI)
{
  XslCompiledTransform xslt = new XslCompiledTransform();

  // This load works just fine, if I provide the path to "Main.xsl".
  // The xsl:include is automatically resolved.
  xslTransform.Load(xslFileURI);

  StringWriter sw = new StringWriter();
  xslt.Transform(Server.MapPath("~/XML/input.xml"), null, sw);
  return sw.ToString();
}

The problem is that I receive the contents of the Main.xsl file as a string and need to load the string as an XmlReader/IXpathNavigable. This is a necessary restriction at this time. When I try to do the same using an XmlReader/XpathDocument, it fails because the code looks for "Included.xsl" in the C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\ folder! Obviously, the XmlResolver is not able to resolve the relative URL because it only receives a string as input XSL.

My efforts in this direction look like:

// This doesn't work! Halp!
private string Transform(string xslContents)
{
  XslCompiledTransform xslt = new XslCompiledTransform();
  XmlUrlResolver resolver = new XmlUrlResolver();
  resolver.Credentials = CredentialCache.DefaultCredentials;

  //METHOD 1: This method does not work.
  XmlReaderSettings settings = new XmlReaderSettings();
  settings.XmlResolver = resolver;
  XmlReader xR = XmlReader.Create(new StringReader(xslContents), settings);
  xslt.Load(xR);    // fails

  // METHOD 2: Does not work either.
  XPathDocument xpDoc = new XPathDocument(new StringReader(xslContents));
  xslt.Load(xpDoc, new XsltSettings(true, true), resolver);  //fails.

  StringWriter sw = new StringWriter();
  xslt.Transform(Server.MapPath("~/XML/input.xml"), null, sw);
  return sw.ToString();
}

I have tried to use the ResolveUri method of the XmlUrlResolver to obtain a Stream referencing the XSL file to be included, but am confused as to how to use this Stream. IOW, how do I tell the XslCompiledTransform object to use this stream in addition to the Main.xsl XmlReader:

Uri mainURI = new Uri(Request.PhysicalApplicationPath + "Main.xsl");
Uri uri = resolver.ResolveUri(mainURI, "Included.xsl");

// I can verify that the Included.xsl file loads in the Stream below.
Stream s = resolver.GetEntity(uri, null, typeof(Stream)) as Stream;

// How do I use this Stream in the function above??


Any help is greatly appreciated. Sorry for the long post!

For your reference, the Exception StackTrace looks like this:

[FileNotFoundException: Could not find file 'C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\Included.xsl'.]
   System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath) +328
   System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy) +1038
   System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize) +113
   System.Xml.XmlDownloadManager.GetStream(Uri uri, ICredentials credentials) +78
   System.Xml.XmlUrlResolver.GetEntity(Uri absoluteUri, String role, Type ofObjectToReturn) +51
   System.Xml.Xsl.Xslt.XsltLoader.CreateReader(Uri uri, XmlResolver xmlResolver) +22
   System.Xml.Xsl.Xslt.XsltLoader.LoadStylesheet(Uri uri, Boolean include) +33
   System.Xml.Xsl.Xslt.XsltLoader.LoadInclude() +349
   System.Xml.Xsl.Xslt.XsltLoader.LoadRealStylesheet() +704
   System.Xml.Xsl.Xslt.XsltLoader.LoadDocument() +293
   System.Xml.Xsl.Xslt.XsltLoader.LoadStylesheet(XmlReader reader, Boolean include) +173
Eoin answered 15/6, 2009 at 11:24 Comment(1)
I'm working on something similar to what your question seems to require and I found an MSDN article – Resolving the Unknown: Building Custom XmlResolvers in the .NET Framework – that seems to provide a very promising solution.Therontheropod
V
7

Use a custom XmlUrlResolver

class MyXmlUrlResolver : XmlUrlResolver
    {
        public override Uri ResolveUri(Uri baseUri, string relativeUri)
        {
            if (baseUri != null)
                return base.ResolveUri(baseUri, relativeUri);
            else
                return base.ResolveUri(new Uri("http://mypath/"), relativeUri);
        }
    }

And use it in load function of XslCompiledTransform,

resolver=new MyXmlUrlResolver();
xslt.Load(xR,null,resolver);
Volvox answered 15/6, 2009 at 12:28 Comment(2)
Karthik: Thanks for the reply. This is the direction I'm currently pursuing. I'm wondering if there is a way to avoid hard coding the "mypath" part in the custom XmlUrlResolver. Any ideas ?Eoin
That can be a configurable parameter or if it is hosted on the same server use Server.MapPath. BTW, How do you get the Main.xsl? By accessing a HTTP path?Volvox
T
5

As Gee's answer mentions, you want to use a custom XmlResolver (of which XmlUrlResolver is already derived), but if you also override the method GetEntity you can resolve references in the primary XSLT document in fun and interesting ways. A deliberately simple example of how you could resolve the reference to Included.xsl:

public class CustomXmlResolver : XmlResolver
{
    public CustomXmlResolver() { }

    public override ICredentials Credentials
    {
        set { }
    }

    public override object GetEntity(Uri absoluteUri, string role, Type ofObjectToReturn)
    {
        MemoryStream entityStream = null;

        switch (absoluteUri.Scheme)
        {
            case "custom-scheme":

                string absoluteUriOriginalString = absoluteUri.OriginalString;
                string ctgXsltEntityName = absoluteUriOriginalString.Substring(absoluteUriOriginalString.IndexOf(":") + 1);
                string entityXslt = "";

                // TODO: Replace the following with your own code to load data for referenced entities.
                switch (ctgXsltEntityName)
                {
                    case "Included.xsl":
                        entityXslt = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\n  <xsl:template name=\"Included\">\n\n  </xsl:template>\n</xsl:stylesheet>";
                        break;
                }

                UTF8Encoding utf8Encoding = new UTF8Encoding();
                byte[] entityBytes = utf8Encoding.GetBytes(entityXslt);
                entityStream = new MemoryStream(entityBytes);

                break;
        }

        return entityStream;
    }

    public override Uri ResolveUri(Uri baseUri, string relativeUri)
    {
        // You might want to resolve all reference URIs using a custom scheme.
        if (baseUri != null)
            return base.ResolveUri(baseUri, relativeUri);
        else
            return new Uri("custom-scheme:" + relativeUri);
    }
}

When you load the Main.xsl document you'd change the relevant code to the following:

xslt.Load(xpDoc, new XsltSettings(true, true), new CustomXmlResolver());

The above example was based on info I picked-up in the MSDN article Resolving the Unknown: Building Custom XmlResolvers in the .NET Framework.

Therontheropod answered 31/7, 2012 at 14:2 Comment(0)
I
2

I am probably missing the obvious but is there a reason you don't just change the URI of Included.xsl to be a true URL? This could either be done in the XSL doc if you have access or using string manipulation otherwise?

Incurve answered 15/6, 2009 at 11:38 Comment(4)
David: Thanks for the reply. The reason is that I cannot hard code any paths anywhere in the application, as a general rule. In this case, it would be my last resort. ;-)Eoin
I am not sure that it can be avoided. The stream example works because you are loading Main.xsl from the same physical location as the Include.xsl. Hence going back to string manipulation you could just add the Request.PhysicalApplicationPath to the URI Otherwise, how will the code know where to look for Include.xsl? It is always going to need the extra pointer as you are coming from a string TnxIncurve
Hmmm... I was unable to do it by deriving a Custom XmlUrlResolver (which was the cleaner way). Because of time constraints, I'm going to have to do it via String manipulation. Thanks for the idea.Eoin
my pleasure, sorry I couldn't give you a cleaner wayIncurve
O
0

I already have success with doing transformations using all in memory:

Having a xslt containing the following includes:

import href="Common.xslt" and import href="Xhtml.xslt"

    private string Transform(string styleSheet, string xmlToParse)
            {
                XslCompiledTransform xslt = new XslCompiledTransform();

                MemoryResourceResolver resolver = new MemoryResourceResolver();            


                XmlTextReader xR = new XmlTextReader(new StringReader(styleSheet));           

                xslt.Load(xR, null, resolver);

                StringWriter sw = new StringWriter();                


                using (var inputReader = new StringReader(xmlToParse))
                {
                    var input = new XmlTextReader(inputReader);
                    xslt.Transform(input,
                                        null,
                                        sw);
                }

                return sw.ToString();

            }     

    public class MemoryResourceResolver : XmlResolver
        {

            public override object GetEntity(Uri absoluteUri,
              string role, Type ofObjectToReturn)
            {
                if (absoluteUri.ToString().Contains("Common"))
                {
                    return new MemoryStream(Encoding.UTF8.GetBytes("Xml with with common data"));
                }

                if (absoluteUri.ToString().Contains("Xhtml"))
                {
                    return new MemoryStream(Encoding.UTF8.GetBytes("Xml with with xhtml data"));
                }         

                return "";
            }
        }

Note that absolutely all content is as strings: styleSheet, xmlToParse and the content of the "Common" and "Xhtml" imports

Ovoid answered 19/10, 2015 at 16:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.