// UnmanagedXslTransform // // Authors: // Tim Coleman // Gonzalo Paniagua Javier (gonzalo@ximian.com) // Ben Maurer (bmaurer@users.sourceforge.net) // // (C) Copyright 2002 Tim Coleman // (c) 2003 Ximian Inc. (http://www.ximian.com) // (C) Ben Maurer 2003 // // DO NOT MOVE THIS FILE. WE WANT HISTORY using System; using System.Collections; using System.IO; using System.Security.Policy; using System.Text; using System.Runtime.InteropServices; using System.Xml.XPath; using BF = System.Reflection.BindingFlags; namespace System.Xml.Xsl { internal unsafe sealed class UnmanagedXslTransform : XslTransformImpl { #region Fields IntPtr stylesheet; Hashtable extensionObjectCache = new Hashtable(); #endregion #region Constructors public UnmanagedXslTransform () { stylesheet = IntPtr.Zero; } #endregion #region Methods ~UnmanagedXslTransform () { FreeStylesheetIfNeeded (); } void FreeStylesheetIfNeeded () { if (stylesheet != IntPtr.Zero) { xsltFreeStylesheet (stylesheet); stylesheet = IntPtr.Zero; } } public override void Load (string url, XmlResolver resolver) { FreeStylesheetIfNeeded (); stylesheet = xsltParseStylesheetFile (url); Cleanup (); if (stylesheet == IntPtr.Zero) throw new XmlException ("Error creating stylesheet"); } public override void Load (XmlReader stylesheet, XmlResolver resolver, Evidence evidence) { FreeStylesheetIfNeeded (); // Create a document for the stylesheet XmlDocument doc = new XmlDocument (); doc.Load (stylesheet); // Store the XML in a StringBuilder StringWriter sr = new UTF8StringWriter (); XmlTextWriter writer = new XmlTextWriter (sr); doc.Save (writer); this.stylesheet = GetStylesheetFromString (sr.GetStringBuilder ().ToString ()); Cleanup (); if (this.stylesheet == IntPtr.Zero) throw new XmlException ("Error creating stylesheet"); } public override void Load (XPathNavigator stylesheet, XmlResolver resolver, Evidence evidence) { FreeStylesheetIfNeeded (); StringWriter sr = new UTF8StringWriter (); Save (stylesheet, sr); this.stylesheet = GetStylesheetFromString (sr.GetStringBuilder ().ToString ()); Cleanup (); if (this.stylesheet == IntPtr.Zero) throw new XmlException ("Error creating stylesheet"); } static IntPtr GetStylesheetFromString (string xml) { IntPtr result = IntPtr.Zero; IntPtr xmlDoc = xmlParseDoc (xml); if (xmlDoc == IntPtr.Zero) { Cleanup (); throw new XmlException ("Error parsing stylesheet"); } result = xsltParseStylesheetDoc (xmlDoc); Cleanup (); if (result == IntPtr.Zero) throw new XmlException ("Error creating stylesheet"); return result; } IntPtr ApplyStylesheet (IntPtr doc, string[] argArr, Hashtable extobjects) { if (stylesheet == IntPtr.Zero) throw new XmlException ("No style sheet!"); IntPtr result; if (extobjects == null || extobjects.Count == 0) { // If there are no extension objects, use the simple (old) method. result = xsltApplyStylesheet (stylesheet, doc, argArr); } else { // If there are extension objects, create a context and register the functions. IntPtr context = xsltNewTransformContext(stylesheet, doc); if (context == IntPtr.Zero) throw new XmlException("Error creating transformation context."); try { foreach (string ns in extobjects.Keys) { object ext = extobjects[ns]; if (extensionObjectCache.ContainsKey(ext)) { foreach (ExtensionFunctionHolder ef in (ArrayList)extensionObjectCache[ext]) { int ret = xsltRegisterExtFunction(context, ef.name, ef.ns, ef.func); if (ret != 0) throw new XmlException("Could not reregister extension function " + ef.name + " in " + ef.ns); } } else { object extsrc; System.Type type; System.Collections.IEnumerable methods; // As an added bonus, if the extension object is a UseStaticMethods object // (defined below), then add the static methods of the specified type. if (ext is UseStaticMethods) { type = ((UseStaticMethods)ext).Type; methods = type.GetMethods(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public); extsrc = null; } else { extsrc = ext; type = ext.GetType(); methods = type.GetMethods(); } ArrayList functionstocache = new ArrayList(); Hashtable alreadyadded = new Hashtable (); foreach (System.Reflection.MethodInfo mi in methods) { if (alreadyadded.ContainsKey(mi.Name)) continue; // don't add twice alreadyadded[mi.Name] = 1; // Simple extension function delegate ExtensionFunction func = new ExtensionFunction(new ReflectedExtensionFunction(type, extsrc, mi.Name).Function); // Delegate for libxslt library call libxsltXPathFunction libfunc = new libxsltXPathFunction(new ExtensionFunctionWrapper(func).Function); int ret = xsltRegisterExtFunction(context, mi.Name, ns, libfunc); if (ret != 0) throw new XmlException("Could not register extension function " + mi.DeclaringType.FullName + "." + mi.Name + " in " + ns); ExtensionFunctionHolder efh; efh.name = mi.Name; efh.ns = ns; efh.func = libfunc; functionstocache.Add(efh); } extensionObjectCache[ext] = functionstocache; } } result = xsltApplyStylesheetUser(stylesheet, doc, argArr, null, IntPtr.Zero, context); } finally { xsltFreeTransformContext(context); } } if (result == IntPtr.Zero) throw new XmlException ("Error applying style sheet"); return result; } static void Cleanup () { xsltCleanupGlobals (); xmlCleanupParser (); } static string GetStringFromDocument (IntPtr doc, IntPtr stylesheet) { IntPtr mem = IntPtr.Zero; int size = 0; int res = xsltSaveResultToString (ref mem, ref size, doc, stylesheet); if (res == -1) throw new XmlException ("xsltSaveResultToString () failed."); string docStr = Marshal.PtrToStringAnsi (mem, size); Marshal.FreeHGlobal (mem); return docStr; } string ApplyStylesheetAndGetString (IntPtr doc, string[] argArr, Hashtable extobjects) { IntPtr xmlOutput = ApplyStylesheet (doc, argArr, extobjects); string strOutput = GetStringFromDocument (xmlOutput, stylesheet); xmlFreeDoc (xmlOutput); return strOutput; } IntPtr GetDocumentFromNavigator (XPathNavigator nav) { StringWriter sr = new UTF8StringWriter (); Save (nav, sr); IntPtr xmlInput = xmlParseDoc (sr.GetStringBuilder ().ToString ()); if (xmlInput == IntPtr.Zero) throw new XmlException ("Error getting XML from input"); return xmlInput; } public override void Transform (XPathNavigator input, XsltArgumentList args, XmlWriter output, XmlResolver resolver) { if (input == null) throw new ArgumentNullException ("input"); if (output == null) throw new ArgumentNullException ("output"); StringWriter writer = new UTF8StringWriter (); IntPtr inputDoc = GetDocumentFromNavigator (input); string[] argArr = null; Hashtable extensionObjects = null; if (args != null) { extensionObjects = args.extensionObjects; argArr = new string[args.parameters.Count * 2 + 1]; int index = 0; foreach (object key in args.parameters.Keys) { argArr [index++] = key.ToString(); object value = args.parameters [key]; if (value is Boolean) argArr [index++] = XmlConvert.ToString((bool) value); // FIXME: How to encode it for libxslt? else if (value is Double) argArr [index++] = XmlConvert.ToString((double) value); // FIXME: How to encode infinity's and Nan? else argArr [index++] = "'" + value.ToString() + "'"; // FIXME: How to encode "'"? } argArr[index] = null; } string transform = ApplyStylesheetAndGetString (inputDoc, argArr, extensionObjects); xmlFreeDoc (inputDoc); Cleanup (); writer.Write (transform); writer.Flush (); output.WriteRaw (writer.GetStringBuilder ().ToString ()); output.Flush (); } public override void Transform(string inputfile, string outputfile, XmlResolver resolver) { IntPtr xmlDocument = IntPtr.Zero; IntPtr resultDocument = IntPtr.Zero; try { xmlDocument = xmlParseFile (inputfile); if (xmlDocument == IntPtr.Zero) throw new XmlException ("Error parsing input file"); resultDocument = ApplyStylesheet (xmlDocument, null, null); if (-1 == xsltSaveResultToFilename (outputfile, resultDocument, stylesheet, 0)) throw new XmlException ("Error in xsltSaveResultToFilename"); } finally { if (xmlDocument != IntPtr.Zero) xmlFreeDoc (xmlDocument); if (resultDocument != IntPtr.Zero) xmlFreeDoc (resultDocument); Cleanup (); } } static void Save (XmlReader rdr, TextWriter baseWriter) { XmlTextWriter writer = new XmlTextWriter (baseWriter); while (rdr.Read ()) { switch (rdr.NodeType) { case XmlNodeType.CDATA: writer.WriteCData (rdr.Value); break; case XmlNodeType.Comment: writer.WriteComment (rdr.Value); break; case XmlNodeType.DocumentType: writer.WriteDocType (rdr.Value, null, null, null); break; case XmlNodeType.Element: writer.WriteStartElement (rdr.Name, rdr.Value); while (rdr.MoveToNextAttribute ()) writer.WriteAttributes (rdr, true); break; case XmlNodeType.EndElement: writer.WriteEndElement (); break; case XmlNodeType.ProcessingInstruction: writer.WriteProcessingInstruction (rdr.Name, rdr.Value); break; case XmlNodeType.Text: writer.WriteString (rdr.Value); break; case XmlNodeType.Whitespace: writer.WriteWhitespace (rdr.Value); break; case XmlNodeType.XmlDeclaration: writer.WriteStartDocument (); break; } } writer.Close (); } static void Save (XPathNavigator navigator, TextWriter writer) { XmlTextWriter xmlWriter = new XmlTextWriter (writer); WriteTree (navigator, xmlWriter); xmlWriter.WriteEndDocument (); xmlWriter.Flush (); } // Walks the XPathNavigator tree recursively static void WriteTree (XPathNavigator navigator, XmlTextWriter writer) { WriteCurrentNode (navigator, writer); if (navigator.MoveToFirstNamespace (XPathNamespaceScope.Local)) { do { WriteCurrentNode (navigator, writer); } while (navigator.MoveToNextNamespace (XPathNamespaceScope.Local)); navigator.MoveToParent (); } if (navigator.MoveToFirstAttribute ()) { do { WriteCurrentNode (navigator, writer); } while (navigator.MoveToNextAttribute ()); navigator.MoveToParent (); } if (navigator.MoveToFirstChild ()) { do { WriteTree (navigator, writer); } while (navigator.MoveToNext ()); navigator.MoveToParent (); if (navigator.NodeType != XPathNodeType.Root) writer.WriteEndElement (); } else if (navigator.NodeType == XPathNodeType.Element) { writer.WriteEndElement (); } } // Format the output static void WriteCurrentNode (XPathNavigator navigator, XmlTextWriter writer) { switch (navigator.NodeType) { case XPathNodeType.Root: writer.WriteStartDocument (); break; case XPathNodeType.Namespace: if (navigator.Name == String.Empty) writer.WriteAttributeString ("xmlns", navigator.Value); else writer.WriteAttributeString ("xmlns", navigator.Name, "http://www.w3.org/2000/xmlns/", navigator.Value); break; case XPathNodeType.Attribute: writer.WriteAttributeString (navigator.Name, navigator.Value); break; case XPathNodeType.Comment: writer.WriteComment (navigator.Value); break; case XPathNodeType.Element: writer.WriteStartElement (navigator.Name); break; case XPathNodeType.ProcessingInstruction: writer.WriteProcessingInstruction (navigator.Name, navigator.Value); break; case XPathNodeType.Text: writer.WriteString (navigator.Value); break; case XPathNodeType.SignificantWhitespace: case XPathNodeType.Whitespace: writer.WriteWhitespace (navigator.Value); break; } } // Extension Objects internal delegate object ExtensionFunction(object[] args); private struct ExtensionFunctionHolder { public libxsltXPathFunction func; public string ns, name; } // Wraps an ExtensionFunction into a function that is callable from the libxslt library. private unsafe class ExtensionFunctionWrapper { private readonly ExtensionFunction func; public ExtensionFunctionWrapper(ExtensionFunction func) { if ((object)func == null) throw new ArgumentNullException("func"); this.func = func; } public unsafe void Function(IntPtr xpath_ctxt, int nargs) { // Convert XPath arguments into "managed" arguments System.Collections.ArrayList args = new System.Collections.ArrayList(); for (int i = 0; i < nargs; i++) { xpathobject* aptr = valuePop(xpath_ctxt); if (aptr->type == 2) // Booleans args.Add( xmlXPathCastToBoolean(aptr) == 0 ? false : true ); else if (aptr->type == 3) // Doubles args.Add( xmlXPathCastToNumber(aptr)); else if (aptr->type == 4) // Strings args.Add( xmlXPathCastToString(aptr)); else if (aptr->type == 1 && aptr->nodesetptr != null) { // Node Sets ==> ArrayList of strings System.Collections.ArrayList a = new System.Collections.ArrayList(); for (int ni = 0; ni < aptr->nodesetptr->count; ni++) { xpathobject *n = xmlXPathNewNodeSet(aptr->nodesetptr->nodes[ni]); valuePush(xpath_ctxt, n); xmlXPathStringFunction(xpath_ctxt, 1); a.Add(xmlXPathCastToString(valuePop(xpath_ctxt))); xmlXPathFreeObject(n); } args.Add(a); } else { // Anything else => string valuePush(xpath_ctxt, aptr); xmlXPathStringFunction(xpath_ctxt, 1); args.Add(xmlXPathCastToString(valuePop(xpath_ctxt))); } xmlXPathFreeObject(aptr); } args.Reverse(); object ret = func(args.ToArray()); // Convert the result back to an XPath object if (ret == null) // null => "" valuePush(xpath_ctxt, xmlXPathNewCString("")); else if (ret is bool) // Booleans valuePush(xpath_ctxt, xmlXPathNewBoolean((bool)ret ? 1 : 0)); else if (ret is int || ret is long || ret is double || ret is float || ret is decimal) // Numbers valuePush(xpath_ctxt, xmlXPathNewFloat((double)ret)); else // Everything else => String valuePush(xpath_ctxt, xmlXPathNewCString(ret.ToString())); } } // Provides a delegate for calling a late-bound method of a type with a given name. // Determines method based on types of arguments. private class ReflectedExtensionFunction { System.Type type; object src; string methodname; public ReflectedExtensionFunction(System.Type type, object src, string methodname) { this.type = type; this.src = src; this.methodname = methodname; } public object Function(object[] args) { // Construct arg type array, and a stringified version in case of problem System.Type[] argtypes = new System.Type[args.Length]; string argtypelist = null; for (int i = 0; i < args.Length; i++) { argtypes[i] = (args[i] == null ? typeof(object) : args[i].GetType() ); if (argtypelist != null) argtypelist += ", "; argtypelist += argtypes[i].FullName; } if (argtypelist == null) argtypelist = ""; // Find the method System.Reflection.MethodInfo mi = type.GetMethod(methodname, (src == null ? BF.Static : BF.Instance | BF.Static) | BF.Public, null, argtypes, null); // No method? if (mi == null) throw new XmlException("No applicable function for " + methodname + " takes (" + argtypelist + ")"); if (!mi.IsStatic && src == null) throw new XmlException("Attempt to call static method without instantiated extension object."); // Invoke return mi.Invoke(src, args); } } // Special Mono-specific class that allows static methods of a type to // be bound without needing an instance of that type. Useful for // registering System.Math functions, for example. // Usage: args.AddExtensionObject( new XslTransform.UseStaticMethods(typeof(thetype)) ); public sealed class UseStaticMethods { public readonly System.Type Type; public UseStaticMethods(System.Type Type) { this.Type = Type; } } #endregion #region Calls to external libraries // libxslt [DllImport ("xslt")] static extern IntPtr xsltParseStylesheetFile (string filename); [DllImport ("xslt")] static extern IntPtr xsltParseStylesheetDoc (IntPtr docPtr); [DllImport ("xslt")] static extern IntPtr xsltApplyStylesheet (IntPtr stylePtr, IntPtr DocPtr, string[] argPtr); [DllImport ("xslt")] static extern int xsltSaveResultToString (ref IntPtr stringPtr, ref int stringLen, IntPtr docPtr, IntPtr stylePtr); [DllImport ("xslt")] static extern int xsltSaveResultToFilename (string URI, IntPtr doc, IntPtr styleSheet, int compression); [DllImport ("xslt")] static extern void xsltCleanupGlobals (); [DllImport ("xslt")] static extern void xsltFreeStylesheet (IntPtr cur); // libxml2 [DllImport ("xml2")] static extern IntPtr xmlNewDoc (string version); [DllImport ("xml2")] static extern int xmlSaveFile (string filename, IntPtr cur); [DllImport ("xml2")] static extern IntPtr xmlParseFile (string filename); [DllImport ("xml2")] static extern IntPtr xmlParseDoc (string document); [DllImport ("xml2")] static extern void xmlFreeDoc (IntPtr doc); [DllImport ("xml2")] static extern void xmlCleanupParser (); [DllImport ("xml2")] static extern void xmlDocDumpMemory (IntPtr doc, ref IntPtr mem, ref int size); [DllImport ("xml2")] static extern void xmlFree (IntPtr data); // Functions and structures for extension objects [DllImport ("xslt")] static extern IntPtr xsltNewTransformContext (IntPtr style, IntPtr doc); [DllImport ("xslt")] static extern void xsltFreeTransformContext (IntPtr context); [DllImport ("xslt")] static extern IntPtr xsltApplyStylesheetUser (IntPtr stylePtr, IntPtr DocPtr, string[] argPtr, string output, IntPtr profile, IntPtr context); [DllImport ("xslt")] static extern int xsltRegisterExtFunction (IntPtr context, string name, string uri, libxsltXPathFunction function); [DllImport ("xml2")] unsafe static extern xpathobject* valuePop (IntPtr context); [DllImport ("xml2")] unsafe static extern void valuePush (IntPtr context, xpathobject* data); [DllImport("xml2")] unsafe static extern void xmlXPathFreeObject(xpathobject* obj); [DllImport("xml2")] unsafe static extern xpathobject* xmlXPathNewCString(string str); [DllImport("xml2")] unsafe static extern xpathobject* xmlXPathNewFloat(double val); [DllImport("xml2")] unsafe static extern xpathobject* xmlXPathNewBoolean(int val); [DllImport("xml2")] unsafe static extern xpathobject* xmlXPathNewNodeSet(IntPtr nodeptr); [DllImport("xml2")] unsafe static extern int xmlXPathCastToBoolean(xpathobject* val); [DllImport("xml2")] unsafe static extern double xmlXPathCastToNumber(xpathobject* val); [DllImport("xml2")] unsafe static extern string xmlXPathCastToString(xpathobject* val); [DllImport("xml2")] static extern void xmlXPathStringFunction(IntPtr context, int nargs); private delegate void libxsltXPathFunction(IntPtr xpath_ctxt, int nargs); private struct xpathobject { public int type; public xmlnodelist* nodesetptr; } private struct xmlnodelist { public int count; public int allocated; public IntPtr* nodes; } #endregion // This classes just makes the base class use 'encoding="utf-8"' class UTF8StringWriter : StringWriter { static Encoding encoding = new UTF8Encoding (false); public override Encoding Encoding { get { return encoding; } } } } }