"Execution of the 'document()' function was prohibited." where EnableDocumentFunction set to true? - c#

I am getting an intermittent System.Xml.Xsl.XslTransformException exception in our production environment when attempting an xslt transform, unfortunately I cannot replicate this in the development environment.
The exception spits out further details:
Execution of the 'document()' function was prohibited. Use the
XsltSettings.EnableDocumentFunction property to enable it. An error
occurred at C:\path\to\file\CDS.xsl(16,3).
However the EnableDocumentFunction property is set to true in the processing code:
private void Transform()
{
var keepTrying = true;
var tryCount = 0;
const int maxRetrys = 3;
while (keepTrying)
{
try
{
var xmlResolver = new XmlUrlResolver();
using (var xmlFile = new XmlNodeReader(_xDoc))
{
var settings = new XmlReaderSettings
{
XmlResolver = xmlResolver,
ProhibitDtd = false,
DtdProcessing = DtdProcessing.Ignore
};
using (var xsl = XmlReader.Create(_xslPath, settings))
{
var xslt = new XslCompiledTransform(true);
xslt.Load(xsl, new XsltSettings { EnableDocumentFunction = true }, xmlResolver);
var sb = new StringBuilder();
using (var writer = XmlWriter.Create(sb, xslt.OutputSettings))
{
xslt.Transform(xmlFile, null, writer, xmlResolver); // errors out here.
}
var xhtml = sb.ToString();
_transformedXml = xhtml;
_isTransformed = true;
xsl.Close();
}
}
keepTrying = false;
}
catch (System.Xml.Xsl.XsltException ex)
{
ExceptionPolicy.HandleException(ex, "ExceptionLogging");
tryCount++;
if (tryCount > maxRetrys)
{
keepTrying = false;
throw;
}
}
}
}
The xslt file is provided by a third party and automatically updated, so rewriting it is not an option. Here is the top of it, slightly modified for privacy reasons:
<?xml version="1.0"?>
<!--
Interaction_550.xsl : 20110916
-->
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:example="http://www.example.com" version="1.0">
<xsl:param name="D2DSeverityFilter"></xsl:param>
<xsl:param name="D2HSeverityFilter"></xsl:param>
<xsl:param name="DocumentationFilter"></xsl:param>
<xsl:output method="html"/>
<xsl:key name="d2d_sev_level-lookup" match="example:d2d_sev_level" use="#name"/>
<xsl:key name="d2h_sev_level-lookup" match="example:d2h_sev_level" use="#name"/>
<xsl:key name="d2l_sev_level-lookup" match="example:d2l_sev_level" use="#name"/>
<xsl:key name="preg_cat-lookup" match="example:preg_cat" use="#cat"/>
<xsl:key name="doc_level-lookup" match="example:doc_level" use="#name"/>
<xsl:variable name="d2d_sev_level-top" select="document('')/*/example:d2d_sev_levels"/>
<xsl:variable name="d2h_sev_level-top" select="document('')/*/example:d2h_sev_levels"/>
<xsl:variable name="d2l_sev_level-top" select="document('')/*/example:d2l_sev_levels"/>
<xsl:variable name="doc_level-top" select="document('')/*/example:doc_levels"/>
<xsl:variable name="preg_cat-top" select="document('')/*/example:preg_cats"/>
<xsl:template match="/">
<head>
<style type="text/css">
body {
font-family : arial,sans serif,helvetica;
}
...
How can I:
fix this so that it does not happen at all?
failing that, how could I go about replicating this in dev?

Alternatively with MS XslCompiledTransform class you can use a XsltSettings class to avoid this error as it described by the error itself. from MSDN

Here is a general way to get rid of the document('')/*/someName expressions:
The original transformation is processed with a special transformation that generates an equivalent transformation that doesn't contain the document('') function call.
Then you just need to use the generated transformation.
Example:
This transformation (lets name it tA), containing document(''):
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="my:my" >
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<my:paramDoc1>
<x>123</x>
<y>37</y>
</my:paramDoc1>
<my:paramDoc2>
<x>456</x>
<y>79</y>
</my:paramDoc2>
<xsl:variable name="vpDoc1" select="document('')/*/my:paramDoc1"/>
<xsl:variable name="vpDoc2" select="document('')/*/my:paramDoc2"/>
<xsl:template match="/*">
<xsl:value-of select="$vpDoc1/x"/>
<xsl:text>|</xsl:text>
<xsl:value-of select="$vpDoc2/y"/>
</xsl:template>
</xsl:stylesheet>
when applied on any document, produces:
123|79
Now we process the above transformation with this one (tGen):
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:x="a:a" exclude-result-prefixes="x">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:namespace-alias stylesheet-prefix="x"
result-prefix="xsl"/>
<xsl:variable name="vApos">'</xsl:variable>
<xsl:variable name="vSelectPrefix" select=
"concat('document(', $vApos,$vApos, ')/*/')"/>
<xsl:template match="node()|#*" name="identity">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="xsl:stylesheet">
<x:stylesheet xmlns:ext="http://exslt.org/common">
<xsl:copy-of select="#*"/>
<xsl:apply-templates/>
</x:stylesheet>
</xsl:template>
<xsl:template match="xsl:variable">
<xsl:variable name="vSelattr" select="#select"/>
<xsl:choose>
<xsl:when test="not(starts-with(#select, $vSelectPrefix))">
<xsl:call-template name="identity"/>
</xsl:when>
<xsl:otherwise>
<x:variable name="vrtf{#name}">
<xsl:copy-of select=
"/*/*[name()
= substring-after($vSelattr, $vSelectPrefix)
]"/>
</x:variable>
<x:variable name="{#name}" select=
"ext:node-set($vrtf{#name})/*"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
The result is a new transformation (tRes), which doesn't contain expressions like document(''):
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common">
<xsl:output omit-xml-declaration="yes"
indent="yes" xmlns:my="my:my" />
<xsl:strip-space elements="*" xmlns:my="my:my" />
<my:paramDoc1 xmlns:my="my:my">
<x>123</x>
<y>37</y>
</my:paramDoc1>
<my:paramDoc2 xmlns:my="my:my">
<x>456</x>
<y>79</y>
</my:paramDoc2>
<xsl:variable name="vrtfvpDoc1">
<my:paramDoc1 xmlns:my="my:my">
<x>123</x>
<y>37</y>
</my:paramDoc1>
</xsl:variable>
<xsl:variable name="vpDoc1" select="ext:node-set($vrtfvpDoc1)/*" />
<xsl:variable name="vrtfvpDoc2">
<my:paramDoc2 xmlns:my="my:my">
<x>456</x>
<y>79</y>
</my:paramDoc2>
</xsl:variable>
<xsl:variable name="vpDoc2" select="ext:node-set($vrtfvpDoc2)/*" />
<xsl:template match="/*" xmlns:my="my:my">
<xsl:value-of select="$vpDoc1/x" />
<xsl:text>|</xsl:text>
<xsl:value-of select="$vpDoc2/y" />
</xsl:template>
</xsl:stylesheet>
If we now apply this dynamically generated transformation (tRes) to any XML document, the result is exactly the same as when applying the original transformation (tA) to this document:
123|79
Therefore, we can use tGen to convert a transformation containing document('') expressions to an equivalent transformation that doesn't contain such expressions.

Related

c# XDocument : Check if particular node name exists , if not add

I have below node which needs to be added in xslt if not exists :-
<xsl:template name="URLSpliter">
<xsl:param name="url" />
<xsl:variable name="splitURL" select="substring-after($url, '/')" />
<xsl:if test="contains($splitURL, '/')">
<!--To call the template recursively-->
<xsl:call-template name="URLSpliter">
<xsl:with-param name="url" select="$splitURL" />
</xsl:call-template>
</xsl:if>
<xsl:if test="not(contains($splitURL, '/'))">
<xsl:value-of select="$splitURL" />
</xsl:if>
</xsl:template>
For this, first I need to check if it exists or not ?-
I have checked it through -
IEnumerable<XElement> xElements = from xmlAuthor in doc.Descendants()
let xElement = xmlAuthor.Element("URLSpliter")
where xElement != null
select xmlAuthor;
var IsUrlSplitterExists= xElements.Any();
if(IsUrlSplitterExists)
{
}
1.I want to know if its correct way or not?
If not exists (element [name="URLSpliter"]) then needs to add.
How can I add it as a first node of xslt?
To select such elements in the XSLT namespace with LINQ to XML you would use
XNamespace xsl = "http://www.w3.org/1999/XSL/Transform";
if (!doc.Root.Elements(xsl + "template").Where(t => (string)t.Attribute("name") == "URLSplitter").Any()) {
doc.Root.AddFirst(new XElement(xsl + "template", new XAttribute("name", "URLSplitter"), ...))
}
Of course, as XSLT is XML, you might as well use XSLT to manipulate your XSLT:
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:axsl="http://www.w3.org/1999/XSL/Transform-alias"
exclude-result-prefixes="axsl"
version="1.0">
<xsl:namespace-alias stylesheet-prefix="axsl" result-prefix="xsl"/>
<xsl:output indent="yes"/>
<xsl:param name="new-template">
<axsl:template name="URLSpliter">
<axsl:param name="url" />
<axsl:variable name="splitURL" select="substring-after($url, '/')" />
<axsl:if test="contains($splitURL, '/')">
<!--To call the template recursively-->
<axsl:call-template name="URLSpliter">
<axsl:with-param name="url" select="$splitURL" />
</axsl:call-template>
</axsl:if>
<axsl:if test="not(contains($splitURL, '/'))">
<axsl:value-of select="$splitURL" />
</axsl:if>
</axsl:template>
</xsl:param>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="xsl:transform[not(xsl:template[#name = 'URLSplitter'])] | xsl:stylesheet[not(xsl:template[#name = 'URLSplitter'])]">
<xsl:copy>
<xsl:apply-templates select="#*"/>
<xsl:copy-of select="$new-template"/>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/94rmq5T

C# Delete entire node from XSLT

From C# code I want to delete node from XSLT.
Eg. I have below XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
<xsl:template name="URLSpliter">
<xsl:param name="url" />
<xsl:variable name="splitURL" select="substring - after($url, '/')" />
<xsl:if test="contains($splitURL, '/')">
<xsl:call-template name="URLSpliter">
<xsl:with-param name="url" select="$splitURL" />
</xsl:call-template>
</xsl:if>
<xsl:if test="not(contains($splitURL, '/'))">
<xsl:value-of select="$splitURL" />
</xsl:if>
</xsl:template>
<xsl:output method="xml" omit-xml-declaration="yes" indent="yes" />
Here I want to delete entire urlsplitter node and all the nodes within URLSplitter
Entire <xsl:template name="URLSpliter"> ...</template> should get deleted (All the nodes within + that particular node )
you can use linq to xml and remove it like as below
documentRoot
.Descendants("template")
.Where(ele=> (string) ele.Attribute("name") == "URLSpliter")
.Remove();
Working sample :
XElement documentRoot =
XElement.Parse (#"<ordersreport date='2012-08-01'>
<returns>
<template name='URLSpliter'>
</template>
<amount>
<orderid>2</orderid>
<orderid>3</orderid>
<orderid>21</orderid>
<orderid>23</orderid>
</amount>
</returns>
</ordersreport>");
documentRoot
.Descendants("template")
.Where(ele=> (string) ele.Attribute("name") == "URLSpliter")
.Remove();
Console.WriteLine(documentRoot.ToString());
This piece of code will work for you. Just replace the path accordingly.
string xsltPath = #"C:\Users\ankushjain\Documents\Visual Studio 2017\Projects\ConsoleApp1\ConsoleApp1\XSLTFile.xslt";
string pathToSave = #"C:\Users\ankushjain\Documents\Visual Studio 2017\Projects\ConsoleApp1\ConsoleApp1\{0}.xslt";
XmlDocument xslDoc = new XmlDocument();
xslDoc.Load(xsltPath);
XmlNamespaceManager namespaceManager = new XmlNamespaceManager(xslDoc.NameTable);
namespaceManager.AddNamespace("xsl", "http://www.w3.org/1999/XSL/Transform");
var nodesToDelete = xslDoc.SelectNodes("//xsl:template[#name='URLSpliter']", namespaceManager);
if (nodesToDelete != null & nodesToDelete.Count > 0)
{
for (int i = nodesToDelete.Count - 1; i >= 0; i--)
{
nodesToDelete[i].ParentNode.RemoveChild(nodesToDelete[i]);
}
xslDoc.Save(string.Format(pathToSave, Guid.NewGuid()));
}

Merge two xml file nodes with same node value

I want to merge nodes of two xml files in C# or XSLT. If the Path value of Method nodes of two different xml files are same. The two Method nodes should be merged as one in the output.
Example:
File1:
<Methods>
<Method>
<ID>1234</ID>
<Name>manager</Name>
<Path>path1</Path>
</Method>
</Methods>
File2:
<Methods>
<Method>
<Path>path1</Path>
<Description>text</Description>
</Method>
</Methods>
Output:
<Methods>
<Method>
<ID>1234</ID>
<Name>manager</Name>
<Description>text</Description>
</Method>
</Methods>
Try it this way?
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="file2" select="document('file2.xml')" />
<xsl:key name="method-by-path" match="Method" use="Path" />
<!-- identity transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Method">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
<xsl:variable name="path" select="Path"/>
<!-- switch context to the other file -->
<xsl:for-each select="$file2">
<xsl:copy-of select="key('method-by-path', $path)/*[not(self::Path)]" />
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Note: this does not check for duplicate nodes.
LINQ to XML is quite effective. Try
var xml1 = XDocument.Load("File1.xml");
var xml2 = XDocument.Load("File2.xml");
foreach (XElement metNode in xml1.Descendants("ID"))
{
metNode.AddAfterSelf(xml2.Descendants("Path").Where(ele => ele.Value.Equals(metNode.Parent.Element("Path").Value)).FirstOrDefault().Parent.Element("Description"));
}

XSLT Conditional Template transformation

I am having one XMl, I want to remove some portion of it based upon a condition.
the XML is as follows :
<?xml version="1.0" encoding="UTF-8"?>
<Root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Header>
<MessageID>0a9d3ba5-9025-4e24-a0de-2403a0c0919b</MessageID>
<MessageDate>2015-04-10</MessageDate>
<PPMVersion>6.0.0.0</PPMVersion>
<SchemaVersion>1.0</SchemaVersion>
</Header>
<Package xsi:type="Ritter_Sales_Template" Path="/Package/Product/Launch_Entity" BusinessID="010170" ID="a13b6861-bb32-47fa-8ef4-f0ddcc0bc134">
<Name>MOTO_pack</Name>
<Category_ID Product_Line_ID="5">5</Category_ID>
<Effective_Start_Date>2015-02-23</Effective_Start_Date>
<Available_Start_Date>2015-02-23</Available_Start_Date>
<Element_Guid>a13b6861-bb32-47fa-8ef4-f0ddcc0bc134</Element_Guid>
<Element_Type_Guid>f7e69de1-23e8-4a66-b57b-d849e989e1ce</Element_Type_Guid>
<Business_ID>010170</Business_ID>
<Product_Name>MOTO_pack</Product_Name>
<Product_To_Product xsi:type="Product_Relation" ID="38224fc0-1a65-4b8e-9985-2277c99bbebf" Pattern="Relation_Entity">
<Association_Start_Date>2015-02-23</Association_Start_Date>
<Association_End_Date>2015-04-15</Association_End_Date>
<Product xsi:type="New_Test_template" Path="/Component/Product/Launch_Entity" BusinessID="009154" ID="da80790a-0523-472c-89b0-208d919fcf73">
<Name>idea_test</Name>
<Category_ID Product_Line_ID="5">8</Category_ID>
<Effective_Start_Date>2015-02-19</Effective_Start_Date>
<Available_Start_Date>2015-02-19</Available_Start_Date>
<Element_Guid>da80790a-0523-472c-89b0-208d919fcf73</Element_Guid>
<Element_Type_Guid>0a7fb9ea-95c4-4eed-a716-ca588f8dcae8</Element_Type_Guid>
<Business_ID>009154</Business_ID>
<Product_Name>idea_test</Product_Name>
<charge_role xsi:type="Lookup_Charge_Role" ID="6920b661-63de-45a0-94d4-900a4a873632" Pattern="Lookup">
<Name>Recipient</Name>
<Description>Recipient</Description>
</charge_role>
</Product>
</Product_To_Product>
<Product_To_Product xsi:type="Product_Relation" ID="eae5a5fd-1a27-4832-bdbe-c77907a77538" Pattern="Relation_Entity">
<Association_Start_Date>2015-02-23</Association_Start_Date>
<Association_End_Date>2015-04-10</Association_End_Date>
<Product xsi:type="Component_Template" Path="/Product_Component/Component/Product/Launch_Entity" BusinessID="010169" ID="9f4a5a5b-1cb4-463b-b211-98f08fec2ce7">
<Name>Moto-g</Name>
<Category_ID Product_Line_ID="5">8</Category_ID>
<Effective_Start_Date>2015-02-22</Effective_Start_Date>
<Available_Start_Date>2015-02-23</Available_Start_Date>
<Element_Guid>9f4a5a5b-1cb4-463b-b211-98f08fec2ce7</Element_Guid>
<Element_Type_Guid>26c0d089-259b-444b-842a-e42d21dea778</Element_Type_Guid>
<Business_ID>010169</Business_ID>
<Product_Name>Moto-g</Product_Name>
<Product_To_Product xsi:type="Product_Relation" ID="96ef93fd-ba40-4628-b22a-92c03e606a89" Pattern="Relation_Entity">
<Association_Start_Date>2015-02-23</Association_Start_Date>
<Product xsi:type="New_Test_template" Path="/Component/Product/Launch_Entity" BusinessID="009154" ID="da80790a-0523-472c-89b0-208d919fcf73">
<Name>idea_test</Name>
<Category_ID Product_Line_ID="5">8</Category_ID>
<Effective_Start_Date>2015-02-19</Effective_Start_Date>
<Available_Start_Date>2015-02-19</Available_Start_Date>
<Element_Guid>da80790a-0523-472c-89b0-208d919fcf73</Element_Guid>
<Element_Type_Guid>0a7fb9ea-95c4-4eed-a716-ca588f8dcae8</Element_Type_Guid>
<Business_ID>009154</Business_ID>
<Product_Name>idea_test</Product_Name>
<charge_role xsi:type="Lookup_Charge_Role" ID="6920b661-63de-45a0-94d4-900a4a873632" Pattern="Lookup">
<Name>Recipient</Name>
<Description>Recipient</Description>
</charge_role>
</Product>
</Product_To_Product>
</Product>
</Product_To_Product>
</Package>
</Root>
in here, i want to remove the whole Product_To_Product element if the Association_End_Date with any product is past dated. for that i have written following code in which i am comparing the dates through a C# function, but i am still unable to remove the entity. please suggest where i am going wrong.
XSLT code :
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:msxsl
="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl" xmlns:cs
="urn:cs">
<xsl:output method="xml" version="1.0" encoding="UTF‐8" indent="yes"/>
<msxsl:script language="C#" implements-prefix="cs">
<![CDATA[public static long compareDate(long Asso_End_Date)
{
DateTime date1 = new DateTime(Asso_End_Date);
DateTime date2 = DateTime.Now;
long result;
result = date2.Date.CompareTo(date1.Date);
return(result);
}]]>
</msxsl:script>
<!-- TEMPLATE #1 -->
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<!-- TEMPLATE #2 -->
<xsl:template name="mode1">
<xsl:apply-templates select="Product_To_Product[Association_End_Date]"/>
</xsl:template>
<xsl:template name="mode2">
<xsl:variable name="date"/><xsl:value-of select="Product_To_Product[Association_End_Date]"/>
<xsl:choose>
<xsl:when test="cs:compareDate($date) > 0">
<xsl:call-template name='mode1'/>
</xsl:when>
<xsl:otherwise>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
There are a number of issues with your XSLT. First, as mentioned by Lingamurthy CS in comments, you have a named template, called mode2, but nothing is calling that template, so it never gets used. It should really be changed to a template match
<xsl:template match="Product_To_Product[Association_End_Date]">
Secondly, you have a problem with how you define your date variable.
<xsl:variable name="date"/>
In other words, you don't define it. It is effectively empty. The xsl:value-of that follows the variable declaration won't affect it at all. You should define it like so
<xsl:variable name="date" select="Association_End_Date"/>
Thirdly, there is a problem with your C# function itself
public static long compareDate(long Asso_End_Date)
The parameter has type long, but you are not passing in long, but a string. The function should really look like this, so it can then parse the string
public static long compareDate(string Asso_End_Date)
{
DateTime date1 = DateTime.Parse(Asso_End_Date);
DateTime date2 = DateTime.Now;
long result;
result = date2.Date.CompareTo(date1.Date);
return(result)
}
Also, you probably need to replace > with < when checking the results of the call.
Try this XSLT (also note how the mode1 template is not needed.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl" xmlns:cs="urn:cs">
<xsl:output method="xml" version="1.0" encoding="UTF‐8" indent="yes"/>
<msxsl:script language="C#" implements-prefix="cs">
<![CDATA[public static long compareDate(string Asso_End_Date)
{
DateTime date1 = DateTime.Parse(Asso_End_Date);
DateTime date2 = DateTime.Now;
long result;
result = date2.Date.CompareTo(date1.Date);
return(result);
}]]>
</msxsl:script>
<xsl:template match="node()|#*" name="identity">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Product_To_Product[Association_End_Date]">
<xsl:variable name="date" select="Association_End_Date"/>
<xsl:choose>
<xsl:when test="cs:compareDate($date) <= 0">
<xsl:call-template name="identity" />
</xsl:when>
<xsl:otherwise>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Or better still, put the condition in the template match, to simplify it like so:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl" xmlns:cs="urn:cs">
<xsl:output method="xml" version="1.0" encoding="UTF‐8" indent="yes"/>
<msxsl:script language="C#" implements-prefix="cs">
<![CDATA[public static long compareDate(string Asso_End_Date)
{
DateTime date1 = DateTime.Parse(Asso_End_Date);
DateTime date2 = DateTime.Now;
long result;
result = date2.Date.CompareTo(date1.Date);
return(result);
}]]>
</msxsl:script>
<xsl:template match="node()|#*" name="identity">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Product_To_Product[cs:compareDate(Association_End_Date) > 0]" />
</xsl:stylesheet>

XslCompiledTransform ignores ordering of XPathNodeIterator

I have an XSLT stylesheet that consumes a document and outputs a SOAP message, where the body is in a specific format defined by a WCF data contract (not specified here). The issue is that WCF has a peculiar notion of what constitutes 'alphabetical' ordering and considers the following order to be correct:
AC
Ab
This is because it uses ordinal string comparison internally. The details are not interesting, suffice to say that XSLT <sort> doesn't natively support this ordering but in order to transform an input document whose format may vary, into an acceptable SOAP message, the stylesheet must be able to order the output elements according to this peculiar ordering. I've therefore decided to implement node sorting in a script block. This is part of a C# solution and uses XslCompiledTransform and therefore msxsl:script is available.
Given a stylesheet:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fn="urn:functions"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
exclude-result-prefixes="msxsl exsl"
xmlns:exsl="http://exslt.org/common"
>
<msxsl:script implements-prefix="fn" language="C#">
<![CDATA[
public class OrdinalComparer : IComparer
{
public int Compare(object x, object y)
{
return string.CompareOrdinal((string)x, (string)y);
}
}
public XPathNodeIterator OrdinalSort(XPathNavigator source)
{
var query = source.Compile("/*");
query.AddSort(source.Compile("local-name()"), new OrdinalComparer());
return source.Select(query);
}
]]>
</msxsl:script>
<xsl:template match="Stuff">
<xsl:element name="Body">
<xsl:element name="Request">
<xsl:variable name="sort">
<xsl:apply-templates select="*"/>
</xsl:variable>
<xsl:for-each select="fn:OrdinalSort($sort)">
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:element>
</xsl:element>
</xsl:template>
<xsl:output method="xml" indent="yes"/>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
And an input document:
<?xml version='1.0' encoding='utf-8'?>
<Root>
<Stuff>
<Age></Age>
<AIS></AIS>
<Something></Something>
<BMI></BMI>
</Stuff>
</Root>
I would expect the output to order the innermost elements as follows:
AIS
Age
BMI
Something
This does not happen. Instead the elements are emitted in the order they went in. Debugging into the stylesheet as it executes I can see the OrdinalSort function is called, and the iterator it returns does enumerate the elements in the desired order, but the XSLT processor somehow ignores this and emits the elements in the order they were encountered.
I have verified additionally that parsing the document in a console application and running the same iterator query emits elements in the right order.
Why, and what can I do about it? The only hunch I have at the moment is that the XSLT engine is interpreting the parent Navigator of the iterator (which hasn't changed from what was passed into the sort function) as the element to reproduce, and ignoring the contents of the iterator.
I am not sure how to solve that with XPathNodeIterator or XPathNavigator, I went as far as creating an XPathNavigator[] from the XPathNodeIterator, to avoid any lazy evaluation effects, but somehow I always ended up with the same result as you.
So as an alternative I have written some code using the DOM implementation in the .NET framework to create some new nodes in the right sort order:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:mf="urn:functions"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
exclude-result-prefixes="msxsl exsl mf"
xmlns:exsl="http://exslt.org/common"
>
<msxsl:script implements-prefix="mf" language="C#">
<![CDATA[
public class OrdinalComparer : IComparer
{
public int Compare(object x, object y)
{
return string.CompareOrdinal((string)x, (string)y);
}
}
public XPathNavigator OrdinalSort(XPathNavigator source)
{
var query = source.Compile("/root/*");
query.AddSort("local-name()", new OrdinalComparer());
XPathNodeIterator result = source.Select(query);
XmlDocument resultDoc = new XmlDocument();
XmlDocumentFragment frag = resultDoc.CreateDocumentFragment();
foreach (XPathNavigator item in result)
{
frag.AppendChild(resultDoc.ReadNode(item.ReadSubtree()));
}
return frag.CreateNavigator();
}
]]>
</msxsl:script>
<xsl:output method="xml" indent="yes"/>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Stuff">
<Body>
<Request>
<xsl:variable name="sort-rtf">
<root>
<xsl:copy-of select="*"/>
</root>
</xsl:variable>
<xsl:variable name="sort" select="exsl:node-set($sort-rtf)"/>
<xsl:variable name="sorted" select="mf:OrdinalSort($sort)"/>
<xsl:copy-of select="$sorted"/>
</Request>
</Body>
</xsl:template>
</xsl:stylesheet>
Using that approach then the result is
<Root>
<Body><Request><AIS /><Age /><BMI /><Something /></Request></Body>
</Root>
I have slightly streamlined the code to
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:mf="urn:functions"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
exclude-result-prefixes="msxsl exsl mf"
xmlns:exsl="http://exslt.org/common"
>
<msxsl:script implements-prefix="mf" language="C#">
<![CDATA[
public class OrdinalComparer : IComparer
{
public int Compare(object x, object y)
{
return string.CompareOrdinal((string)x, (string)y);
}
}
public XPathNavigator OrdinalSort(XPathNavigator source)
{
var query = source.Compile("*");
query.AddSort("local-name()", new OrdinalComparer());
XPathNodeIterator result = source.Select(query);
XmlDocument resultDoc = new XmlDocument();
XmlDocumentFragment frag = resultDoc.CreateDocumentFragment();
foreach (XPathNavigator item in result)
{
frag.AppendChild(resultDoc.ReadNode(item.ReadSubtree()));
}
return frag.CreateNavigator();
}
]]>
</msxsl:script>
<xsl:output method="xml" indent="yes"/>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Stuff">
<Body>
<Request>
<xsl:variable name="sorted" select="mf:OrdinalSort(.)"/>
<xsl:copy-of select="$sorted"/>
</Request>
</Body>
</xsl:template>
</xsl:stylesheet>
Having to construct XmlNodes in the extension function C# "script" seems like an overhead but I am not sure how to solve it otherwise.
I have devised a hideous workaround to the original problem - which was to make XSLT support character ordinal sorting. I consider this an answer, but definitely not a good one. The following snippet illustrates this solution:
<xsl:template match="Stuff">
<xsl:element name="Body">
<xsl:element name="Request">
<xsl:variable name="source">
<xsl:apply-templates select="*"/>
</xsl:variable>
<xsl:for-each select="exsl:node-set($source)/*">
<xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 1, 1))"/>
<xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 2, 2))"/>
<xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 3, 3))"/>
<xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 4, 4))"/>
<xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 5, 5))"/>
<xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 6, 6))"/>
<xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 7, 7))"/>
<xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 8, 8))"/>
<xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 9, 9))"/>
<xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 10, 10))"/>
<xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 11, 11))"/>
<xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 12, 12))"/>
<xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 13, 13))"/>
<xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 14, 14))"/>
<xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 15, 15))"/>
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:element>
</xsl:element>
</xsl:template>
Where the extension function GetOrdinal is as follows:
public int GetOrdinal(string s)
{
return s.Length == 1 ? (char)s[0] : 0;
}
This is quite frankly shameful, and I would not advocate ever doing anything as shoddy as this. But it works.

Categories