2
votes

I'm trying to break up a large XML book into multiple files (by chapter). This sandbox XML:

<?xml version="1.0" encoding="UTF-8"?>
<book>
    <chapter id='1'>Chapter 1</chapter>
        <p>Highly random content.</p>
        <div>foo</div>
        <blockquote>Bar</blockquote>
    <chapter id='2'>Chapter 2</chapter>
        <div>
          <p>p in <i>italicized div</i>!</p>
        </div>
        <p>Content.</p>
    <chapter id='3'>Chapter 3</chapter>
       <blockquote>foo</blockquote>
       <p>foo</p>
    <chapter id='4'>Chapter 4</chapter>
       <p>foo</p>
      <p>end of book</p>
</book>

Transforms as desired by this XSLT:

<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" />
<xsl:strip-space elements="*"/>    
<xsl:template match="book">
    <xsl:for-each-group select="*" group-starting-with="chapter"> 
        <xsl:result-document href="{concat('/home/paul/t',@id,'.xhtml')}">
            <xsl:for-each select="current-group()[self::*]">
                <xsl:copy-of select="."/>
            </xsl:for-each>
        </xsl:result-document>
    </xsl:for-each-group>
</xsl:template>

Problem. When I apply the XSLT to a more complex production document, it doesn't work. I'd put the source doc here..but it's too large, and the client may not like that anyway.

I'm puzzled as to why it doesn't work. Instead of making 1 file per chapter like the sandbox test, it makes one large file. Suspect the problem may be related to the @id in the filename. On a production document, the @id comes up null. The result file name comes out as t.xhtml with the entire book in it, when I really wanted multiple files of t --id of the chapter-- .xhtml.

Any hints where to look? Is there a way I could make the code above more resilent to varying document complexity (I think that is my problem...the move from the simple concept to production has introduced something I'm not catching, maybe some tighter code would help).

2
We really need to see a sample that causes the problem, not simply one that does work as expected. Are you sure the chapter elements you are trying to match with the pattern group-starting-with="chapter" are not in some namespace? If the result is a single file then it looks as if the grouping creates a single group and that would happen if the pattern does not match at all. - Martin Honnen
As @Martin suggests the error occurs if the current group is not a <chapter>. This happens if either the document does not contain any chapter OR if there are other tags under <book> but before the first <chapter>. These will be selected as one group without @ID. At least that's what Saxon does for your input file with extra preceeding tags inserted. I second @Martin: we need some (at least shortened) realistic input. - Marcus Rickert
Thanks for feedback. I haven't quite fixed it yet, but your thoughts have pointed me in the direction where the problem is (I think). I'm not very good at debugging yet.. - Paulb

2 Answers

1
votes

As for your question about how to make your code more resilient you could try to check for the most obvious errors in the input file which will make your XSLT process run into errors or at least produce unusable output:

  • Check for other tags before the first <chapter>
  • Check for <chapter> without @id
  • Check for <chapter>'s with non-unique @id

The following XSLT will execute these checks and issue a warning. Otherwise it will produce the output files.

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

    <xsl:strip-space elements="*"/>    

    <xsl:template match="book">

      <xsl:variable name="chapter_ids" >
        <xsl:for-each-group select="chapter" group-by="@id">
          <count_me/>
        </xsl:for-each-group>
      </xsl:variable>

      <xsl:choose>

        <xsl:when test="name(*[1]) != 'chapter'">
          <xsl:message>The book does not start with chapter! --- Aborting</xsl:message>
        </xsl:when>

        <xsl:when test="count(chapter[normalize-space(@id) = '']) > 0">
          <xsl:message>The book contains chapters without chapter ids! - Aborting</xsl:message>
        </xsl:when>

        <xsl:when test="count(chapter) &gt; count($chapter_ids/count_me)">
          <xsl:message>The book has more chapters (<xsl:value-of select="count(chapter)"/>) than chapter ids (<xsl:value-of select="count($chapter_ids/count_me)"/>)! - Aborting</xsl:message>
        </xsl:when>

        <xsl:otherwise>

          <xsl:for-each-group select="*" group-starting-with="chapter"> 
            <xsl:result-document href="{concat('./t',@id,'.xhtml')}">
              <xsl:for-each select="current-group()[self::*]">
                <xsl:copy-of select="."/>
              </xsl:for-each>
            </xsl:result-document>
          </xsl:for-each-group>

        </xsl:otherwise>

      </xsl:choose>

    </xsl:template>
</xsl:stylesheet>
0
votes

The behaviour you're experiencing indicates that the XPath group-starting-with="chapter" selects nothing. I believe that the real content contains a difference in the tag name. Modifying the word chapter to an arbitrary literal makes the issue reproducible on the sample data.