XSLT: обход древовидной структуры

У меня есть XML-документ со списком категорий:

<categories>
    <category id="1" parent="0">Configurations</category>
    <category id="11" parent="13">LCD Monitor</category>
    <category id="12" parent="13">CRT Monitor</category>
    <category id="13" parent="1"">Monitors</category>
    <category id="123" parent="122">Printer</category>
    ...
</categories>

И список продуктов:

<products>
  <product>
    ...
   <category>12</category>
    ...
  </product>
    ...
</products>

Если категория товара равна 12, то ее нужно преобразовать в "Конфигурации/Мониторы/ЭЛТ-монитор" (брать категорию 12, потом родительскую (13) и т.д.). Если родитель равен 0, остановитесь.

Есть ли элегантный способ сделать это с помощью преобразования XSL?


person Mindaugas Mozūras    schedule 12.03.2009    source источник


Ответы (4)


Я не знаю, будет ли это считаться элегантным, но с этим вводом:

<root>
    <categories>
        <category id="1" parent="0">Configurations</category>
        <category id="11" parent="13">LCD Monitor</category>
        <category id="12" parent="13">CRT Monitor</category>
        <category id="13" parent="1">Monitors</category>
        <category id="123" parent="122">Printer</category>
    </categories>
    <products>
        <product>
             <category>12</category>
        </product>
        <product>
             <category>11</category>
        </product>
     </products>
</root>

Этот XSLT:

<?xml version="1.0"?>

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

<xsl:template match="/">
  <root>
  <xsl:apply-templates select="//product"/>
  </root>
</xsl:template>

<xsl:template match="product">
  <product>
    <path>
      <xsl:call-template name="catwalk">
        <xsl:with-param name="id"><xsl:value-of select="category"/>
        </xsl:with-param>
      </xsl:call-template>
    </path>
  </product>
</xsl:template>

<xsl:template name="catwalk">
  <xsl:param name="id"/>
  <xsl:if test="$id != '0'">
    <xsl:call-template name="catwalk">
      <xsl:with-param name="id"><xsl:value-of select="//category[@id = $id]/@parent"/>
      </xsl:with-param>
    </xsl:call-template>
    <xsl:value-of select="//category[@id = $id]"/><xsl:text>/</xsl:text>
  </xsl:if>
</xsl:template>

</xsl:stylesheet>

Дает вам этот вывод:

<?xml version="1.0" encoding="utf-8"?>
  <root>
  <product>
    <path>Configurations/Monitors/CRT Monitor/
    </path>
  </product>
  <product>
     <path>Configurations/Monitors/LCD Monitor/
     </path>
  </product>
  </root>

Пути по-прежнему имеют дополнительную косую черту в конце, вам понадобится еще немного условного XSLT, чтобы косая черта испускалась только тогда, когда вы не находитесь на первом уровне.

Крайне важно, чтобы иерархия категорий была правильной, иначе ваше преобразование может легко попасть в бесконечный цикл, который остановится только тогда, когда закончится память. Если бы я реализовал что-то подобное в реальной системе, у меня возникло бы искушение добавить параметр в шаблон catWalk, который увеличивался бы при каждом вызове, и добавить его в тест, чтобы он перестал зацикливаться после 10 вызовов, независимо от того, был ли найден родитель. .

person andynormancx    schedule 12.03.2009

Рекомендуется использовать <xsl:key>:

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

  <xsl:output method="text" />

  <xsl:key name="category" match="categories/category" use="@id" />

  <xsl:template match="/">
    <xsl:apply-templates select="//products/product" />
  </xsl:template>

  <xsl:template match="product">
    <xsl:apply-templates select="key('category', category)" />
    <xsl:text>&#10;</xsl:text>
  </xsl:template>

  <xsl:template match="category">
    <xsl:if test="@parent &gt; 0">
      <xsl:apply-templates select="key('category', @parent)" />
      <xsl:text>/</xsl:text>
    </xsl:if>
    <xsl:value-of select="."/>
  </xsl:template>

</xsl:stylesheet>

Это производит:

Configurations/Monitors/LCD Monitor
Configurations/Monitors/CRT Monitor

при тестировании на следующем XML:

<data>
  <categories>
    <category id="1" parent="0">Configurations</category>
    <category id="11" parent="13">LCD Monitor</category>
    <category id="12" parent="13">CRT Monitor</category>
    <category id="13" parent="1">Monitors</category>
    <category id="123" parent="122">Printer</category>
  </categories>
  <products>
    <product>
      <category>11</category>
    </product>
    <product>
      <category>12</category>
    </product>
  </products>
</data>
person Tomalak    schedule 12.03.2009

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

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

  <xsl:output omit-xml-declaration="yes"/>

  <xsl:template match="/">
    <xsl:call-template name="OutputCategoryTree">
      <xsl:with-param name="productId" select="12"/>
    </xsl:call-template>
  </xsl:template>

  <xsl:template name="OutputCategoryTree">
    <xsl:param name="productId"/>
    <xsl:variable name="parentId" select="/categories/category[@id=$productId]/@parent"/>
    <xsl:if test="$parentId!=0"> 
      <xsl:call-template name="OutputCategoryTree">
        <xsl:with-param name="productId" select="/categories/category[@id=$productId]/@parent"/>
      </xsl:call-template>
    </xsl:if>/
    <xsl:value-of select="/categories/category[@id=$productId]"/>
  </xsl:template>
</xsl:stylesheet>

Извините за грубый пример кода, но он генерирует

/Конфигурации/Мониторы/ЭЛТ-монитор

person Yossi Dahan    schedule 12.03.2009

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

<xsl:template match="categories">
    <categories>
        <xsl:apply-templates select="category[@parent='0']"/>
    </categories>
</xsl:template>

<xsl:template match="category">
    <category id='{@id}'>
       <xsl:value-of select="text()"/>
       <xsl:apply-templates select="/categories/category[@parent=current()/@id]"/>
    </category>
</xsl:template>

Это произведет что-то вроде этого:

<categories>
    <category id="1">Configurations
       <category id="13">Monitors
          <category id="11">LCD Monitor</category>
           <category id="12">CRT Monitor</category>
       </category>
    </category>
    ...
</categories>

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

<xsl:template match="product"/>
   <xsl:variable name="c" select="$categories/categories/category[@id=current()/category]"/>
   <xsl:foreach select="$c/ancestor-or-self::category">
      <xsl:value-of select="text()"/>
      <xsl:if test="position() != last()">
         <xsl:text>/</xsl:text>
      </xsl:if>
   </xsl:foreach>
</xsl:template>
person Robert Rossney    schedule 12.03.2009