0

How to make work a solution described here when I change the XML structure to have multiple good entities like that:

<?xml version="1.0" encoding="ISO-8859-1"?>
<Work>    
<Good id = "1">
    <list  num="1050" id = "2531" desc="List 1">
        <part  num="1">
            <pos  isKey="0" id="2532" pid="2531" desc="Part 1" />
            <pos  num="1.2." isKey="0" id="2554" pid="2532" desc="Position 1.2" />
            <pos  num="1.2.6." isKey="1" id="2591" pid="2554" desc="Position 1.2.6" />
          </part>
        </list>
        <list  num="1090" id = "3029" desc="List 2">
          <part  num="2">
            <pos  isKey="0" id="3033" pid="3029" desc="Category 2" />
            <pos  isKey="0" id="3040" pid="3033" desc="Part 9" />
            <pos  num="9.2." isKey="0" id="3333" pid="3040" desc="Position 9.2" />
            <pos  num="9.2.1." isKey="0" id="3334" pid="3333" desc="Position 9.2.1" />
            <pos  num="9.2.1.2" isKey="1" id="3339" pid="3334" desc="Position 9.2.1.2" />
        </part>
    </list>
</Good>
<Good id = "2">
    <list  num="1050" id = "2531" desc="List 3">
        <part  num="1">
            <pos  isKey="0" id="2532" pid="2531" desc="Part 1" />
            <pos  num="1.2." isKey="0" id="2554" pid="2532" desc="Position 1.2" />
            <pos  num="1.2.6." isKey="0" id="2591" pid="2554" desc="Position 1.2.6" />
            <pos  num="1.2.6.1." isKey="1" id="2592" pid="2591" desc="Position 1.2.6.1" />       
         </part>
     </list>      
 </Good>
</Work>

I tried to make a for-each loop for Work/Good entity, but it doesn't help:

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

  <!-- key to look up any element with an id attribute based on the value of
       that id -->
  <xsl:key name="elementsByPid" match="*[@pid]" use="@pid" />

  <xsl:template match="/">
    <html>
      <body>
      <h2>Lists</h2>
        <xsl:apply-templates select="Work/Goods" />
      </body>
    </html>
  </xsl:template>

<xsl:template match="Work/Goods">
    <xsl:for-each select="Work/Goods">
        <xsl:value-of select="."/>
    </xsl:for-each>
    <xsl:apply-templates select="Work/Goods/list"  />
</xsl:template>

<xsl:template match="Work/Goods/list">
    <xsl:for-each select="Work/Goods/list">
        <xsl:value-of select="."/>
    </xsl:for-each>
    <xsl:apply-templates select="."  mode="table"/>
</xsl:template>

<xsl:template match="*" mode="table">
    <xsl:variable name="shouldOutput">
      <xsl:apply-templates select="." mode="shouldOutput" />
    </xsl:variable>
    <xsl:if test="string-length($shouldOutput)">
      <table>
        <xsl:apply-templates select="." />
      </table>
    </xsl:if>
  </xsl:template>

  <!-- the main recursive logic - first produce output for this row, then
       process any of the children (in the id->pid chain) that need to be
       output -->
  <xsl:template match="*">
    <xsl:apply-templates select="." mode="row" />
    <xsl:for-each select="key('elementsByPid', @id)">
      <xsl:variable name="shouldOutput">
        <xsl:apply-templates select="." mode="shouldOutput" />
      </xsl:variable>
      <xsl:if test="string-length($shouldOutput)">
        <xsl:apply-templates select="." />
      </xsl:if>
    </xsl:for-each>
  </xsl:template>

  <xsl:template match="*" mode="row">
    <tr>
      <td colspan="2"><xsl:value-of select="@desc" /></td>
    </tr>
  </xsl:template>

  <!-- special case for pos elements with a @num - produce two columns -->
  <xsl:template match="pos[@num]" mode="row">
    <tr>
      <td><xsl:value-of select="@num" /></td>
      <td><xsl:value-of select="@desc" /></td>
    </tr>
  </xsl:template>

  <!-- check whether this node should be output by checking whether it, or any
       of its descendants in the id->pid tree, has @out=1.  The template will
       return an empty RTF for nodes that should not be output, and an RTF
       containing a text node with one or more "1" characters for nodes that
       should.  -->
  <xsl:template match="*[@out='1']" mode="shouldOutput">1</xsl:template>
  <xsl:template match="*" mode="shouldOutput">
    <xsl:apply-templates select="key('elementsByPid', @id)"
         mode="shouldOutput"/>
  </xsl:template>

</xsl:stylesheet>
enter code here

There in the templates that doesn't allow this code to work.
What else should I change to make it work?

Community
  • 1
  • 1
Illania
  • 117
  • 1
  • 1
  • 14
  • Two things that came to my mind - a) Please dumb down your code. This is a frightening wall of text, not a nice, concise question. Reduce it to the actual problem, don't post all the code you have. b) Please *format* your code. Use proper indentation, kill the blank lines, avoid lines that need horizontal scrolling. Your code looks like you don't care for it very much - so why should anyone else? – Tomalak Oct 15 '13 at 13:18
  • Very nice, now that's looking a lot friendlier! :) – Tomalak Oct 15 '13 at 13:56

3 Answers3

2

You don't need to use any for-each for this, just remove the two templates

<xsl:template match="Work/Goods">

<xsl:template match="Work/Goods/list">

completely, and change the root template to say simply

  <xsl:template match="/">
    <html>
      <body>
      <h2>Lists</h2>
        <xsl:apply-templates select="Work/Good/list" mode="table" />
      </body>
    </html>
  </xsl:template>

There's no need for explicit iteration, as the single select expression will pull out all the list elements inside all the Good elements (note your XML example has the elements named Good rather than Goods) inside the Work element.

Ian Roberts
  • 120,891
  • 16
  • 170
  • 183
  • I just tried your solution and for me it produces `

    Lists

    `. Can you give me a hint on what I am doing wrong?
    – hielsnoppe Oct 15 '13 at 16:52
  • @hielsnoppe I misread the XML - the XSLT in the question talks about elements named "Goods" but the sample input has them called "Good" without the trailing s. The rest of the XSLT in the question is taken from [my answer to the OP's previous question](http://stackoverflow.com/a/19359402/592139). – Ian Roberts Oct 15 '13 at 17:09
  • I see, looks good then! I will study your solution to learn from it and wonder if you can you tell me in contrast to my approach which advantages e.g. in performance or style (like "the way one does things" in XSLT) it has? Thank you for sharing your experience! – hielsnoppe Oct 15 '13 at 18:34
1

I am not sure, whether this is what you mean, but I hope the following is useful to you. My suggestion is to process the document two times. Once with step1.xsl and the result of it with step2.xsl. Explanation on what the transformations do is given at the very end of my answer.

Given an input file the form of the one you provided above, but with the attribute out="1" added to one of the <part /> elements

<?xml version="1.0" encoding="ISO-8859-1"?>
<Work>    
<Good id = "1">
<list  num="1050" id = "2531" desc="List 1">
    <part  num="1">
        <pos  isKey="0" id="2532" pid="2531" desc="Part 1" />
        <pos  num="1.2." isKey="0" id="2554" pid="2532" desc="Position 1.2" />
        <pos  num="1.2.6." isKey="1" id="2591" pid="2554" desc="Position 1.2.6" />
      </part>
    </list>
    <list  num="1090" id = "3029" desc="List 2">
      <part  num="2">
        <pos  isKey="0" id="3033" pid="3029" desc="Category 2" />
        <pos  isKey="0" id="3040" pid="3033" desc="Part 9" />
        <pos  num="9.2." isKey="0" id="3333" pid="3040" desc="Position 9.2" />
        <pos  num="9.2.1." isKey="0" id="3334" pid="3333" desc="Position 9.2.1" out="1" />
        <pos  num="9.2.1.2" isKey="1" id="3339" pid="3334" desc="Position 9.2.1.2" />
    </part>
</list>
</Good>
<Good id = "2">
<list  num="1050" id = "2531" desc="List 3">
    <part  num="1">
        <pos  isKey="0" id="2532" pid="2531" desc="Part 1" />
        <pos  num="1.2." isKey="0" id="2554" pid="2532" desc="Position 1.2" />
        <pos  num="1.2.6." isKey="0" id="2591" pid="2554" desc="Position 1.2.6" />
        <pos  num="1.2.6.1." isKey="1" id="2592" pid="2591" desc="Position 1.2.6.1" />       
     </part>
 </list>      
 </Good>
</Work>

the following transformation step1.xsl

<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output indent="yes" />
    <xsl:strip-space elements="*" />

    <xsl:key name="id" match="pos" use="@id" />
    <xsl:key name="pid" match="pos" use="@pid" />

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

    <xsl:template match="pos[count(key('id', @pid)) = 0]">
        <xsl:variable name="id" select="@id" />
        <xsl:copy>
            <xsl:copy-of select="@*" />
            <xsl:apply-templates select="ancestor::Good//pos[@pid=$id]" mode="nested" />
        </xsl:copy>
    </xsl:template>

    <xsl:template match="pos" />
    <xsl:template match="pos" mode="nested">
        <xsl:variable name="id" select="@id" />
        <xsl:copy>
            <xsl:copy-of select="@*" />
            <xsl:apply-templates select="ancestor::Good//pos[@pid=$id]" mode="nested" />
        </xsl:copy>
    </xsl:template>
</xsl:transform>

produces the following output

<?xml version="1.0"?>
<Work>
  <Good id="1">
    <list num="1050" id="2531" desc="List 1">
      <part num="1">
        <pos isKey="0" id="2532" pid="2531" desc="Part 1">
          <pos num="1.2." isKey="0" id="2554" pid="2532" desc="Position 1.2">
            <pos num="1.2.6." isKey="1" id="2591" pid="2554" desc="Position 1.2.6"/>
          </pos>
        </pos>
      </part>
    </list>
    <list num="1090" id="3029" desc="List 2">
      <part num="2">
        <pos isKey="0" id="3033" pid="3029" desc="Category 2">
          <pos isKey="0" id="3040" pid="3033" desc="Part 9">
            <pos num="9.2." isKey="0" id="3333" pid="3040" desc="Position 9.2">
              <pos num="9.2.1." isKey="0" id="3334" pid="3333" desc="Position 9.2.1" out="1">
                <pos num="9.2.1.2" isKey="1" id="3339" pid="3334" desc="Position 9.2.1.2"/>
              </pos>
            </pos>
          </pos>
        </pos>
      </part>
    </list>
  </Good>
  <Good id="2">
    <list num="1050" id="2531" desc="List 3">
      <part num="1">
        <pos isKey="0" id="2532" pid="2531" desc="Part 1">
          <pos num="1.2." isKey="0" id="2554" pid="2532" desc="Position 1.2">
            <pos num="1.2.6." isKey="0" id="2591" pid="2554" desc="Position 1.2.6">
              <pos num="1.2.6.1." isKey="1" id="2592" pid="2591" desc="Position 1.2.6.1"/>
            </pos>
          </pos>
        </pos>
      </part>
    </list>
  </Good>
</Work>

that taken as an input for the following transformation step2.xsl

<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output indent="yes" />
    <xsl:strip-space elements="*" />

    <xsl:template match="/">
        <xsl:apply-templates match="pos[@out=1]" />
    </xsl:template>

    <xsl:template match="pos[@out=1]">
        <table>
        <tr><td><xsl:value-of select="ancestor::list/@desc" /></td></tr>
        <xsl:for-each select="ancestor-or-self::pos[count(ancestor::pos) &lt; 2]">
            <xsl:sort select="position()" order="descending" />
            <tr><td><xsl:value-of select="@desc" /></td></tr>
        </xsl:for-each>
        <xsl:for-each select="ancestor-or-self::pos[count(ancestor::pos) >= 2]">
            <tr>
                <td><xsl:value-of select="@num" /></td>
                <td><xsl:value-of select="@desc" /></td>
            </tr>
        </xsl:for-each>
        </table>
    </xsl:template>
</xsl:transform>

produces an output that looks like what I think you want

<?xml version="1.0"?>
<table>
  <tr>
    <td>List 2</td>
  </tr>
  <tr>
    <td>Part 9</td>
  </tr>
  <tr>
    <td>Category 2</td>
  </tr>
  <tr>
    <td>9.2.</td>
    <td>Position 9.2</td>
  </tr>
  <tr>
    <td>9.2.1.</td>
    <td>Position 9.2.1</td>
  </tr>
</table>

Please clarify if it does not.

Explanation: The first transformation step1.xsl builds nested hierarchical structures from the flat lists of pos elements based on their @pid attributes. These make it easier to go through the chain of parents in the second pass.

The second transformation step2.xsl then matches on every pos element that should produce output, indicated by its @out attribute and writes out a table of the structure you drafted as ASCII art in your original question.

What is not yet done is merging of identical tables in the case that e.g. two pos[@out=1] elements are contained in the same list.

hielsnoppe
  • 2,819
  • 3
  • 31
  • 56
1

The problem is that you the @pid is not enough to correctly identify <pos> elements, since the same @pid can occur in multiple goods.

This means that you need to correlate each @pid with the <Good> it belongs to in your <xsl:key> (and then, in each use of the key). This can be done by building a unique string from @pid and the enclosing Good/@id.

concat(@pid, '|', ancestor::Good/@id)

What follows is basically is the XSLT from the previous question. I've highlighted the changes.

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

  <xsl:key name="elementsByPid" match="*[@pid]" use="concat(@pid, '|', ancestor::Good/@id)" />
  <!--  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^  -->

  <xsl:template match="/">
    <html>
      <body>
        <h2>Lists</h2>
        <xsl:apply-templates select="/Work/Good/list" mode="table" />
      </body>
    </html>
  </xsl:template>

  <xsl:template match="*" mode="table">
    <xsl:variable name="shouldOutput">
      <xsl:apply-templates select="." mode="shouldOutput" />
    </xsl:variable>
    <xsl:if test="string-length($shouldOutput)">
      <table>
        <xsl:apply-templates select="." />
      </table>
    </xsl:if>
  </xsl:template>

  <xsl:template match="*">
    <xsl:apply-templates select="." mode="row" />
    <xsl:for-each select="key('elementsByPid', concat(@id, '|', ancestor::Good/@id))">
    <!--  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^  -->
      <xsl:variable name="shouldOutput">
        <xsl:apply-templates select="." mode="shouldOutput" />
      </xsl:variable>
      <xsl:if test="string-length($shouldOutput)">
        <xsl:apply-templates select="." />
      </xsl:if>
    </xsl:for-each>
  </xsl:template>

  <xsl:template match="*" mode="row">
    <tr>
      <td colspan="2"><xsl:value-of select="@description" /></td>
    </tr>
  </xsl:template>

  <xsl:template match="pos[@num]" mode="row">
    <tr>
      <td class="num"><xsl:value-of select="@num" /></td>
      <td><xsl:value-of select="@description" /></td>
    </tr>
  </xsl:template>

  <xsl:template match="*[@out='1']" mode="shouldOutput">1</xsl:template>

  <xsl:template match="*" mode="shouldOutput">
    <xsl:apply-templates select="key('elementsByPid', concat(@id, '|', ancestor::Good/@id))" mode="shouldOutput"/>
    <!--  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^  -->
  </xsl:template>

</xsl:stylesheet>

Also see http://www.xmlplayground.com/9iG5oz.


PS: Since you seem to have problems with understanding how <xsl:apply-templates> works, maybe you find an older answer of mine explaining it helpful. I've also written an answer that explains <xsl:key>.

Community
  • 1
  • 1
Tomalak
  • 332,285
  • 67
  • 532
  • 628