У меня есть таблица стилей XSLT, которая расходует документ и выводит сообщение SOAP, где тело находится в определенном формате, определяемом контрактом данных WCF (здесь не указано). Проблема в том, что WCF имеет своеобразное представление о том, что представляет собой "алфавитный" порядок, и считает, что следующий порядок является правильным:
Это происходит потому, что он использует стандартное сравнение строк внутри. Детали не интересны, достаточно сказать, что XSLT <sort>
не поддерживает этот порядок, но для того, чтобы преобразовать входной документ, формат которого может изменяться, в приемлемое сообщение SOAP, таблица стилей должна иметь возможность упорядочить выходные элементы в соответствии с этим особым порядком. Поэтому я решил реализовать сортировку узлов в блоке сценария. Это часть решения С# и использует XslCompiledTransform
и поэтому msxsl:script
.
Учитывая таблицу стилей:
<?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>
И входной документ:
<?xml version='1.0' encoding='utf-8'?>
<Root>
<Stuff>
<Age></Age>
<AIS></AIS>
<Something></Something>
<BMI></BMI>
</Stuff>
</Root>
Я ожидаю, что результат будет упорядочен для самых внутренних элементов следующим образом:
Этого не происходит. Вместо этого элементы испускаются в том порядке, в котором они вошли. Отладка в таблицу стилей по мере ее выполнения я могу увидеть, что вызывается функция OrdinalSort, и итератор, который он возвращает, перечисляет элементы в нужном порядке, но XSLT-процессор каким-то образом игнорирует это и испускает элементы в том порядке, в котором они встречались.
Я дополнительно проверил, что разбор документа в консольном приложении и запуск того же запроса итератора испускают элементы в правильном порядке.
Почему, и что я могу с этим поделать? Единственная догадка, которую я имею на данный момент, заключается в том, что движок XSLT интерпретирует родительский Навигатор итератора (который не изменился с того, что передавалось в функцию сортировки) как элемент для воспроизведения и игнорирует содержимое итератора.
Я не уверен, как решить это с помощью XPathNodeIterator
или XPathNavigator
, я дошел до создания XPathNavigator[]
из XPathNodeIterator
, чтобы избежать каких-либо ленивых эффектов оценки, но почему-то я всегда XPathNodeIterator
тот же результат, что и вы.
Поэтому в качестве альтернативы я написал некоторый код с использованием реализации DOM в платформе.NET для создания некоторых новых узлов в правильном порядке сортировки:
<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>
Используя этот подход, результат
<Root>
<Body><Request><AIS /><Age /><BMI /><Something /></Request></Body>
</Root>
Я немного упростил код для
<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>
При создании XmlNode
в функции расширения С# "скрипт" кажется накладным, но я не уверен, как его решить иначе.
Я разработал отвратительный обходной путь к исходной проблеме, которая заключалась в том, чтобы упорядочить сортировку символов XSLT. Я считаю, что это ответ, но, безусловно, не является хорошим. Следующий фрагмент иллюстрирует это решение:
<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>
Если функция расширения GetOrdinal выглядит следующим образом:
public int GetOrdinal(string s)
{
return s.Length == 1 ? (char)s[0] : 0;
}
Это совершенно откровенно позорно, и я бы не стал выступать за то, чтобы делать что-то такое дрянное, как это. Но это работает.