Вопрос

I have a list of order lines with each one product on them. The products in may form a self-referencing hierarchy. I need to order the lines in such a way that all products that have no parent or whose parent is missing from the order are at the top, followed by their children. No child may be above its parent in the end result.

So how can i order the following xml:

<order>
  <line><product code="3" parent="1"/></line>
  <line><product code="2" parent="1"/></line>
  <line><product code="6" parent="X"/></line>
  <line><product code="1" /></line>
  <line><product code="4" parent="2"/></line>
</order>

Into this:

<order>
  <line><product code="6" parent="X"/></line>
  <line><product code="1" /></line>
  <line><product code="2" parent="1"/></line>
  <line><product code="3" parent="1"/></line>
  <line><product code="4" parent="2"/></line>
</order>

Note that the order within a specific level is not important, as long as the child node follows at some point after it's parent.

I have a solution which works for hierarchies that do not exceed a predefined depth:

<order>

<xsl:variable name="level-0" 
  select="/order/line[ not(product/@parent=../line/product/@code) ]"/>
<xsl:for-each select="$level-0">
   <xsl:copy-of select="."/>
</xsl:for-each>

<xsl:variable name="level-1"
  select="/order/line[ product/@parent=$level-0/product/@code ]"/>
<xsl:for-each select="$level-1">
   <xsl:copy-of select="."/>
</xsl:for-each>

<xsl:variable name="level-2"
  select="/order/line[ product/@parent=$level-1/product/@code ]"/>
<xsl:for-each select="$level-2">
   <xsl:copy-of select="."/>
</xsl:for-each>

</order>

The above sample xslt will work for hierarchies with a maximum depth of 3 levels and is easily extended to more, but how can i generalize this and have the xslt sort arbitrary levels of depth correctly?

Это было полезно?

Решение 2

Very interesting problem. I would do this in two passes: first, nest the elements according to their hierarchy. Then output the elements, sorted by the count of their ancestors.

XSLT 1.0 (+ EXSLT node-set() function):

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

<xsl:key name="product-by-code" match="product" use="@code" />

<!-- first pass -->
<xsl:variable name="nested">
    <xsl:apply-templates select="/order/line/product[not(key('product-by-code', @parent))]" mode="nest"/>
</xsl:variable>

<xsl:template match="product" mode="nest">
    <xsl:copy>
        <xsl:copy-of select="@*"/>
        <xsl:apply-templates select="../../line/product[@parent=current()/@code]" mode="nest"/>
    </xsl:copy>
</xsl:template>

<!-- output -->
<xsl:template match="/order">
    <xsl:copy>
        <xsl:for-each select="exsl:node-set($nested)//product">
        <xsl:sort select="count(ancestor::*)" data-type="number" order="ascending"/>
            <line><product><xsl:copy-of select="@*"/></product></line>
        </xsl:for-each>
    </xsl:copy>
</xsl:template>

</xsl:stylesheet> 

When applied to your input, the result is:

<?xml version="1.0" encoding="UTF-8"?>
<order>
   <line>
      <product code="6" parent="X"/>
   </line>
   <line>
      <product code="1"/>
   </line>
   <line>
      <product code="3" parent="1"/>
   </line>
   <line>
      <product code="2" parent="1"/>
   </line>
   <line>
      <product code="4" parent="2"/>
   </line>
</order>

This still leaves the issue of the existing/missing parent X - I will try to address that later.

Другие советы

To start with, you could define a couple of keys to help you look up the line elements by either their code or parent attribute

<xsl:key name="products-by-parent" match="line" use="product/@parent" />
<xsl:key name="products-by-code" match="line" use="product/@code" />

You would start off by selecting the line elements with no parent, using a key to do this check:

<xsl:apply-templates select="line[not(key('products-by-code', product/@parent))]"/>

Then, within the template that matches the line element, you would just copy the element, and then select its "children" like so, using the other key

<xsl:apply-templates select="key('products-by-parent', product/@code)"/>

This would be a recursive call, so it would recursively look for its children until no more are found.

Try this XSLT

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:output method="xml" indent="yes"/>
   <xsl:key name="products-by-parent" match="line" use="product/@parent"/>
   <xsl:key name="products-by-code" match="line" use="product/@code"/>

   <xsl:template match="order">
      <xsl:copy>
         <xsl:apply-templates select="line[not(key('products-by-code', product/@parent))]"/>
      </xsl:copy>
   </xsl:template>

   <xsl:template match="line">
      <xsl:call-template name="identity"/>
      <xsl:apply-templates select="key('products-by-parent', product/@code)"/>
   </xsl:template>

   <xsl:template match="@*|node()" name="identity">
      <xsl:copy>
         <xsl:apply-templates select="@*|node()"/>
      </xsl:copy>
   </xsl:template>
</xsl:stylesheet>

Do note the use of the XSLT identity transform to copy the existing nodes in the XML.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top