XslCompiledTransform игнорирует порядок XPathNodeIterator

1

У меня есть таблица стилей XSLT, которая расходует документ и выводит сообщение SOAP, где тело находится в определенном формате, определяемом контрактом данных WCF (здесь не указано). Проблема в том, что WCF имеет своеобразное представление о том, что представляет собой "алфавитный" порядок, и считает, что следующий порядок является правильным:

  • переменный ток
  • Ab

Это происходит потому, что он использует стандартное сравнение строк внутри. Детали не интересны, достаточно сказать, что 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>

Я ожидаю, что результат будет упорядочен для самых внутренних элементов следующим образом:

  • АИС
  • Возраст
  • BMI
  • Что нибудь

Этого не происходит. Вместо этого элементы испускаются в том порядке, в котором они вошли. Отладка в таблицу стилей по мере ее выполнения я могу увидеть, что вызывается функция OrdinalSort, и итератор, который он возвращает, перечисляет элементы в нужном порядке, но XSLT-процессор каким-то образом игнорирует это и испускает элементы в том порядке, в котором они встречались.

Я дополнительно проверил, что разбор документа в консольном приложении и запуск того же запроса итератора испускают элементы в правильном порядке.

Почему, и что я могу с этим поделать? Единственная догадка, которую я имею на данный момент, заключается в том, что движок XSLT интерпретирует родительский Навигатор итератора (который не изменился с того, что передавалось в функцию сортировки) как элемент для воспроизведения и игнорирует содержимое итератора.

Теги:
visual-studio
xslt
soap

2 ответа

1

Я не уверен, как решить это с помощью 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 в функции расширения С# "скрипт" кажется накладным, но я не уверен, как его решить иначе.

0

Я разработал отвратительный обходной путь к исходной проблеме, которая заключалась в том, чтобы упорядочить сортировку символов 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;
    }

Это совершенно откровенно позорно, и я бы не стал выступать за то, чтобы делать что-то такое дрянное, как это. Но это работает.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню