альтернативные отсортированные узлы в XSLT 1.0 без функции расширения

Это очень похожий вопрос, как XSL: преобразование xml в отсортированный мультистолбец HTML-таблица

Но (к сожалению) есть дополнительное требование: это должен быть XSLT 1.0 без функций расширения, т.е. без использования функции набора узлов.

Это мой упрощенный XML:

<demo>
  <config n_columns="3" />
  <messages>
    <msg date="2011-07-06" title="2nd message" />
    <title>message list</title>
    <msg date="2011-07-05" title="4th message" />
    <msg date="2011-07-06" title="3rd message" />
    <msg date="2011-07-07" title="1st message" />
  </messages>
</demo>

Используя эту таблицу стилей:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html" />
  <xsl:template match="/">
    <xsl:apply-templates select="demo/messages">
      <xsl:with-param name="n_columns" select="number(/demo/config/@n_columns)" />
    </xsl:apply-templates>
  </xsl:template>
  <xsl:template match="messages">
    <xsl:param name="n_columns" />
    <div>
      <xsl:value-of select="concat(./title, ' (', $n_columns, ' columns)')" />
    </div>
    <table>
      <xsl:variable name="cells" select="msg" />
      <xsl:apply-templates select="$cells[(position() - 1) mod $n_columns = 0]"
        mode="row">
        <xsl:with-param name="n_columns" select="$n_columns" />
        <xsl:with-param name="cells" select="$cells" />
      </xsl:apply-templates>
    </table>
  </xsl:template>
  <xsl:template match="msg" mode="row">
    <xsl:param name="n_columns" />
    <xsl:param name="cells" />
    <xsl:variable name="n_row" select="position()" />
    <xsl:variable name="row_cells"
      select="$cells[position() > ($n_row - 1) * $n_columns][position() &lt;= $n_columns]" />
    <tr>
      <xsl:apply-templates select="$row_cells" mode="cell" />
      <xsl:call-template name="empty-cells">
        <xsl:with-param name="n" select="$n_columns - count($row_cells)" />
      </xsl:call-template>
    </tr>
  </xsl:template>
  <xsl:template match="msg" mode="cell">
    <td>
      <xsl:value-of select="@title" />
    </td>
  </xsl:template>
  <xsl:template name="empty-cells">
    <xsl:param name="n" />
    <xsl:if test="$n > 0">
      <td>
        <xsl:attribute name="colspan">
          <xsl:value-of select="$n" />
        </xsl:attribute>
        <xsl:text>&#xA0;</xsl:text>
      </td>
    </xsl:if>
  </xsl:template>
</xsl:stylesheet>

Производит этот HTML-фрагмент в качестве вывода:

<div>message list (3 columns)</div>
<table>
  <tr>
    <td>2nd message</td>
    <td>4th message</td>
    <td>3rd message</td>
  </tr>
  <tr>
    <td>1st message</td>
    <td colspan="2">&nbsp;</td>
  </tr>
</table>

Чего явно не хватает, так это сортировки...

На самом деле мне нужно переопределить переменную "cells" следующим образом:

<xsl:variable name="cells">
  <xsl:for-each select="msg">
    <xsl:sort select="@date" order="descending" />
    <xsl:sort select="@title" />
    <xsl:copy-of select="." />
  </xsl:for-each>
</xsl:variable>

Но теперь я должен определить другую переменную для преобразования RTF в нодлист и передать ее в шаблон, который я применяю.

<xsl:variable name="sCells" select="ext:node-set($cells)/*" />

В результате получится следующий HTML-фрагмент:

<div>message list (3 columns)</div>
<table>
  <tr>
    <td>1st message</td>
    <td>2nd message</td>
    <td>3rd message</td>
  </tr>
  <tr>
    <td>4th message</td>
    <td colspan="2">&nbsp;</td>
  </tr>
</table>

К сожалению, мой механизм XSLT (инструментарий SAP XML для java) не поддерживает эту (или аналогичную) функцию расширения. Поэтому я ищу другое решение, которое не требует функции расширения набора узлов.

Я провел довольно много времени, читая всевозможные форумы и т. Д., Но я действительно не могу понять это. Возможно, у кого-то есть хорошая идея для альтернативного подхода? Спасибо!


Это продолжение, основанное на (слегка расширенном) решении Дмитрия. Этот ввод XML

<demo>
  <config n_columns="3" />
  <messages>
    <msg date="2011-07-06" title="2nd message" />
    <title>message list</title>
    <msg date="2011-07-05" title="4th message" />
    <msg date="2011-07-06" title="3rd message" />
    <msg date="2011-07-07" title="1st message" />
    <msg date="2011-07-05" title="5th message" />
    <msg date="2011-07-05" title="7th message" />
    <msg date="2011-07-05" title="6th message" />
  </messages>
</demo>

в сочетании с этой таблицей стилей XSLT

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html" />

  <xsl:variable name="vNumCols" select="/*/config/@n_columns" />
  <xsl:variable name="vCells" select="/*/messages/msg" />
  <xsl:variable name="vNumCells" select="count($vCells)" />
  <xsl:variable name="vNumRows" select="ceiling($vNumCells div $vNumCols)" />
  <xsl:variable name="vIndexPatternLength"
    select="string-length(concat('', $vNumCells))" />
  <xsl:variable name="vIndexPattern">
    <xsl:call-template name="padding">
      <xsl:with-param name="length" select="$vIndexPatternLength" />
      <xsl:with-param name="chars" select="'0'" />
    </xsl:call-template>
  </xsl:variable>
  <xsl:variable name="vSortedIndex">
    <xsl:for-each select="$vCells">
      <xsl:sort select="@date" order="descending" />
      <xsl:sort select="@title" />
      <xsl:value-of
        select="format-number(count(preceding-sibling::msg) + 1,
          $vIndexPattern)" />
    </xsl:for-each>
  </xsl:variable>

  <xsl:template match="/">
    <xsl:apply-templates select="demo/messages" />
  </xsl:template>
  <xsl:template match="messages">
    <table>
      <xsl:for-each select="$vCells[not(position() > $vNumRows)]">
        <xsl:variable name="vRow" select="position()" />
        <tr>
          <xsl:for-each select="$vCells[not(position() > $vNumCols)]">
            <xsl:variable name="vCol" select="position()" />
            <xsl:variable name="vCell"
              select="($vRow - 1) * $vNumCols + $vCol" />
            <xsl:variable name="vIndex"
              select="substring($vSortedIndex,
                ($vCell - 1) * $vIndexPatternLength + 1,
                $vIndexPatternLength)" />
            <xsl:variable name="vMessage"
              select="$vCells[position() = $vIndex]" />
            <xsl:choose>
              <xsl:when test="$vMessage">
                <xsl:apply-templates select="$vMessage"
                  mode="cell" />
              </xsl:when>
              <xsl:otherwise>
                <xsl:call-template name="empty-cell" />
              </xsl:otherwise>
            </xsl:choose>
          </xsl:for-each>
        </tr>
      </xsl:for-each>
    </table>
  </xsl:template>

  <xsl:template match="msg" mode="cell">
    <td>
      <xsl:apply-templates select="." />
    </td>
  </xsl:template>
  <xsl:template match="msg">
    <xsl:value-of select="concat(@date, ' : ', @title)" />
  </xsl:template>

  <xsl:template name="empty-cell">
    <td>
      <xsl:text>&#xA0;</xsl:text>
    </td>
  </xsl:template>

  <!-- http://www.exslt.org/str/functions/padding/ -->
  <xsl:template name="padding">
    <xsl:param name="length" select="0" />
    <xsl:param name="chars" select="' '" />
    <xsl:choose>
      <xsl:when test="not($length) or not($chars)" />
      <xsl:otherwise>
        <xsl:variable name="string"
          select="concat($chars, $chars, $chars, $chars, $chars, 
                         $chars, $chars, $chars, $chars, $chars)" />
        <xsl:choose>
          <xsl:when test="string-length($string) >= $length">
            <xsl:value-of select="substring($string, 1, $length)" />
          </xsl:when>
          <xsl:otherwise>
            <xsl:call-template name="padding">
              <xsl:with-param name="length" select="$length" />
              <xsl:with-param name="chars" select="$string" />
            </xsl:call-template>
          </xsl:otherwise>
        </xsl:choose>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

производит этот вывод HTML

<table>
  <tr>
    <td>2011-07-07 : 1st message</td>
    <td>2011-07-06 : 2nd message</td>
    <td>2011-07-06 : 3rd message</td>
  </tr>
  <tr>
    <td>2011-07-05 : 4th message</td>
    <td>2011-07-05 : 5th message</td>
    <td>2011-07-05 : 6th message</td>
  </tr>
  <tr>
    <td>2011-07-05 : 7th message</td>
    <td>&nbsp;</td>
    <td>&nbsp;</td>
  </tr>
</table>

Спасибо, Димитр!


person Ziggy    schedule 06.07.2011    source источник
comment
Поддерживает ли ваш XSLT-процессор встроенные функции расширения Javascript?   -  person Dimitre Novatchev    schedule 06.07.2011
comment
К сожалению нет. Однако он поддерживает пользовательские функции расширения Java. Так что в крайнем случае я мог бы попытаться реализовать свою собственную функцию набора узлов. Но это не кажется таким уж очевидным, и код будет привязан к (с закрытым исходным кодом) реализации процессора XSLT.   -  person Ziggy    schedule 06.07.2011
comment
Хороший вопрос, +1. См. мой ответ для полного решения XSLT 1.0, в котором не используются какие-либо функции расширения. :)   -  person Dimitre Novatchev    schedule 06.07.2011
comment
Я думаю, что простой ответ — не использовать этот XSLT-процессор; Насколько я помню, он уже много лет не ремонтируется.   -  person Julian Reschke    schedule 07.07.2011
comment
@Julian Джулиан Возможно, ты прав, он действительно сильно устарел. Но у меня не было большого выбора. Я использую эту таблицу стилей в контексте WPC (композитор веб-страниц), и внутренняя структура WPC выбирает процессор XSLT без возможности для меня повлиять на это (кроме изменения самой структуры). На самом деле он просто использует TransformerFactory.newInstance(); который получает XSLT-процессор по умолчанию и (по-прежнему) является экземпляром com.sap.engine.lib.jaxp.TransformerFactoryImpl. Я полагаю, что мог бы изменить настройки по умолчанию для всей системы, но... По крайней мере, так обстоит дело на NW7.0.   -  person Ziggy    schedule 08.07.2011
comment
@Ziggy: Мне довелось работать над Netweaver давным-давно, и я ясно помню, что мы перешли на JDK-XSLT. Это может быть неправильная конфигурация вашей системы.   -  person Julian Reschke    schedule 11.07.2011


Ответы (1)


Выполнить требуемую обработку в XSLT 1.0 без использования каких-либо функций расширения сложно, но возможно:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:variable name="vNumCols"
      select="/*/config/@n_columns"/>

 <xsl:variable name="vItems" select="/*/messages/msg"/>

 <xsl:variable name="vNumItems" select="count($vItems)"/>

 <xsl:variable name="vNumRows" select=
   "ceiling($vNumItems div $vNumCols)"/>

 <xsl:variable name="vsortedInds">
  <xsl:for-each select="$vItems">
   <xsl:sort select="@date" order="descending"/>

   <xsl:value-of select=
   "format-number(count(preceding-sibling::msg)+1,
                  '0000'
                  )
   "/>
  </xsl:for-each>
 </xsl:variable>

 <xsl:template match="/">
  <table>
   <xsl:for-each select=
   "$vItems[not(position() > $vNumRows)]">
     <tr>
       <xsl:variable name="vRow" select="position()"/>

       <xsl:for-each select="$vItems[not(position() > $vNumCols)]">
          <xsl:variable name="vcurIndIndex" select=
           "($vRow -1)*$vNumCols + position()"/>
          <xsl:variable name="vcurInd" select=
          "substring($vsortedInds, 4*($vcurIndIndex -1) +1, 4)"/>

          <xsl:variable name="vcurItem" select="$vItems[position()=$vcurInd]"/>
          <xsl:if test="$vcurItem">
           <td>
            <xsl:value-of select="$vcurItem/@title"/>
           </td>
          </xsl:if>
       </xsl:for-each>
     </tr>
   </xsl:for-each>
  </table>
 </xsl:template>
</xsl:stylesheet>

Когда это преобразование применяется к предоставленному XML-документу:

<demo>
    <config n_columns="3" />
    <messages>
        <msg date="2011-07-06" title="2nd message" />
        <title>message list</title>
        <msg date="2011-07-05" title="4th message" />
        <msg date="2011-07-06" title="3rd message" />
        <msg date="2011-07-07" title="1st message" />
    </messages>
</demo>

производится значительная часть желаемого вывода (остальное я оставляю читателю в качестве упражнения :) ):

<table>
   <tr>
      <td>1st message</td>
      <td>2nd message</td>
      <td>3rd message</td>
   </tr>
   <tr>
      <td>4th message</td>
   </tr>
</table>

Пояснение:

  1. Чтобы не преобразовывать RTF в набор узлов, мы используем строку индексов отсортированных элементов. Каждый индекс занимает четыре символа (при необходимости слева дополняется нулями). Затем мы используем эти индексы для заполнения строк таблицы.

  2. Чтобы избежать повторного обращения, мы используем метод Пьеза. итерации через N элементов, не являющихся узлами.

Обратите внимание. Это решение предполагает, что таблица не будет содержать более 9999 ячеек. Если ожидается больше ячеек, вы можете легко изменить код, например:

Заменять:

format-number(count(preceding-sibling::msg)+1,
                      '0000'
                    )

с участием:

format-number(count(preceding-sibling::msg)+1,
                      '00000'
                    )

И заменить:

          <xsl:variable name="vcurInd" select=
          "substring($vsortedInds, 4*($vcurIndIndex -1) +1, 4)"/>

с участием

          <xsl:variable name="vcurInd" select=
          "substring($vsortedInds, 5*($vcurIndIndex -1) +1, 5)"/>
person Dimitre Novatchev    schedule 06.07.2011
comment
Вау, это действительно потрясающе! Он работает, и с первого взгляда я понимаю его логику. Прямо сейчас я немного изучаю этот ответ, прежде чем закрыть вопрос. - person Ziggy; 06.07.2011
comment
@Ziggy: Рад, что вы нашли этот ответ полезным. Здесь, в SO, официальный способ выразить благодарность — принять лучший ответ (щелкнув зеленую галочку рядом с ответом). - person Dimitre Novatchev; 06.07.2011
comment
Да, теперь я действительно понимаю, что происходит и почему это на самом деле работает. Сначала отсортируйте узел и объедините отсортированные индексы в виде строки. В этом примере vSortedInds='0004000100030002'. Затем выполните цикл через vNumRows x vNumColumns, вычислите текущую позицию ячейки, получите индекс из вычисленной подстроки индекса (например, ячейка 2 => '0001'), а затем получите фактический узел из vItems на основе позиции. Сладкий! Я немного расширим его, чтобы сделать его более гибким, но лежащий в его основе алгоритм идеален. Большое спасибо! - person Ziggy; 06.07.2011