Aranxo
Aranxo

Reputation: 1183

XSLT: Wrap some tags with another tag

I got some weird XML which I have to import elsewhere. The structure is like this example:

<INVOICE>
  <HEADER_DATA_1>
    <HEADER_DATA_ITEM_1>foo</HEADER_DATA_ITEM_1>
    <HEADER_DATA_ITEM_2>bar</HEADER_DATA_ITEM_2>
  </HEADER_DATA_1>
  <HEADER_DATA_2>
    <HEADER_DATA_ITEM_3>foo</HEADER_DATA_ITEM_3>
    <HEADER_DATA_ITEM_4>bar</HEADER_DATA_ITEM_4>
  </HEADER_DATA_2>
  <HEADER_DATA_3>
    <HEADER_DATA_ITEM_5>foo</HEADER_DATA_ITEM_5>
    <HEADER_DATA_ITEM_6>bar</HEADER_DATA_ITEM_6>
  </HEADER_DATA_3>

  <POSITION_DATA_1>
    <POSITION_DATA_ITEM_1>foo</POSITION_DATA_ITEM_1>
    <POSITION_DATA_ITEM_2>bar</POSITION_DATA_ITEM_2>
  </POSITION_DATA_1>
  <POSITION_DATA_2>
    <POSITION_DATA_ITEM_3>foo</POSITION_DATA_ITEM_3>
    <POSITION_DATA_ITEM_4>bar</POSITION_DATA_ITEM_4>
  </POSITION_DATA_2>
  [...]
  <POSITION_DATA_8>
    <POSITION_DATA_ITEM_15>foo</POSITION_DATA_ITEM_15>
    <POSITION_DATA_ITEM_16>bar</POSITION_DATA_ITEM_16>
  </POSITION_DATA_8>

  <POSITION_DATA_1>
    <POSITION_DATA_ITEM_1>foo</POSITION_DATA_ITEM_1>
    <POSITION_DATA_ITEM_2>bar</POSITION_DATA_ITEM_2>
  </POSITION_DATA_1>
  <POSITION_DATA_2>
    <POSITION_DATA_ITEM_3>foo</POSITION_DATA_ITEM_3>
    <POSITION_DATA_ITEM_4>bar</POSITION_DATA_ITEM_4>
  </POSITION_DATA_2>
  [...]
  <POSITION_DATA_8>
    <POSITION_DATA_ITEM_15>foo</POSITION_DATA_ITEM_15>
    <POSITION_DATA_ITEM_16>bar</POSITION_DATA_ITEM_16>
  </POSITION_DATA_8>

  <POSITION_DATA_1>
    <POSITION_DATA_ITEM_1>foo</POSITION_DATA_ITEM_1>
    <POSITION_DATA_ITEM_2>bar</POSITION_DATA_ITEM_2>
  </POSITION_DATA_1>
  <POSITION_DATA_2>
    <POSITION_DATA_ITEM_3>foo</POSITION_DATA_ITEM_3>
    <POSITION_DATA_ITEM_4>bar</POSITION_DATA_ITEM_4>
  </POSITION_DATA_2>
  [...]
  <POSITION_DATA_8>
    <POSITION_DATA_ITEM_15>foo</POSITION_DATA_ITEM_15>
    <POSITION_DATA_ITEM_16>bar</POSITION_DATA_ITEM_16>
  </POSITION_DATA_8>

</INVOICE>

As you see, an invoice objekt can have some single header data and some multiple positions which consist in some position data. But sadly these guys who exported this forgot to wrap a proper tag around the position data. So I want to fix it so that the final code would look like this:

<INVOICE>
  <HEADER_DATA_1>
    <HEADER_DATA_ITEM_1>foo</HEADER_DATA_ITEM_1>
    <HEADER_DATA_ITEM_2>bar</HEADER_DATA_ITEM_2>
  </HEADER_DATA_1>
  <HEADER_DATA_2>
    <HEADER_DATA_ITEM_3>foo</HEADER_DATA_ITEM_3>
    <HEADER_DATA_ITEM_4>bar</HEADER_DATA_ITEM_4>
  </HEADER_DATA_2>
  <HEADER_DATA_3>
    <HEADER_DATA_ITEM_5>foo</HEADER_DATA_ITEM_5>
    <HEADER_DATA_ITEM_6>bar</HEADER_DATA_ITEM_6>
  </HEADER_DATA_3>

  <POSITION>
    <POSITION_DATA_1>
      <POSITION_DATA_ITEM_1>foo</POSITION_DATA_ITEM_1>
      <POSITION_DATA_ITEM_2>bar</POSITION_DATA_ITEM_2>
    </POSITION_DATA_1>
    <POSITION_DATA_2>
      <POSITION_DATA_ITEM_3>foo</POSITION_DATA_ITEM_3>
      <POSITION_DATA_ITEM_4>bar</POSITION_DATA_ITEM_4>
    </POSITION_DATA_2>
    [...]
    <POSITION_DATA_8>
      <POSITION_DATA_ITEM_15>foo</POSITION_DATA_ITEM_15>
      <POSITION_DATA_ITEM_16>bar</POSITION_DATA_ITEM_16>
    </POSITION_DATA_8>
  </POSITION>

  <POSITION>
    <POSITION_DATA_1>
      <POSITION_DATA_ITEM_1>foo</POSITION_DATA_ITEM_1>
      <POSITION_DATA_ITEM_2>bar</POSITION_DATA_ITEM_2>
    </POSITION_DATA_1>
    <POSITION_DATA_2>
      <POSITION_DATA_ITEM_3>foo</POSITION_DATA_ITEM_3>
      <POSITION_DATA_ITEM_4>bar</POSITION_DATA_ITEM_4>
    </POSITION_DATA_2>
    [...]
    <POSITION_DATA_8>
      <POSITION_DATA_ITEM_15>foo</POSITION_DATA_ITEM_15>
      <POSITION_DATA_ITEM_16>bar</POSITION_DATA_ITEM_16>
    </POSITION_DATA_8>
  </POSITION>

  <POSITION>
    <POSITION_DATA_1>
      <POSITION_DATA_ITEM_1>foo</POSITION_DATA_ITEM_1>
      <POSITION_DATA_ITEM_2>bar</POSITION_DATA_ITEM_2>
    </POSITION_DATA_1>
    <POSITION_DATA_2>
      <POSITION_DATA_ITEM_3>foo</POSITION_DATA_ITEM_3>
      <POSITION_DATA_ITEM_4>bar</POSITION_DATA_ITEM_4>
    </POSITION_DATA_2>
    [...]
    <POSITION_DATA_8>
      <POSITION_DATA_ITEM_15>foo</POSITION_DATA_ITEM_15>
      <POSITION_DATA_ITEM_16>bar</POSITION_DATA_ITEM_16>
    </POSITION_DATA_8>
  </POSITION>
</INVOICE>

To make it more worse, some elements are optional, especially the POSITION_DATA_1 element. At least POSITION_DATA_2 and POSITION_DATA_8 are mandatory. I fixed the (maybe) missing POSITION_DATA_1 tag like this:

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

  <xsl:template match="POSITION_DATA_2[not(preceding-sibling::POSITION_DATA_1)]">
    <xsl:element name="POSITION_DATA_1"/>
    <xsl:call-template name="identity"/>
  </xsl:template>

which inserts it if missing. For my main goal I tried to adapt the solution from this thread. The rule I tried looks like this:

<xsl:template match="INVOICE">
    <xsl:copy>
        <xsl:apply-templates select="HEADER_DATA_1|HEADER_DATA_2|HEADER_DATA_3"/>
        <POSITION>
            <xsl:apply-templates select="POSITION_DATA_1|POSITION_DATA_2|POSITION_DATA_3|POSITION_DATA_4|POSITION_DATA_5|POSITION_DATA_6|POSITION_DATA_7|POSITION_DATA_8"/>
        </POSITION>
    </xsl:copy>
</xsl:template>

But this won't match. It only wraps one single POSITION tag around all positions. Any suggestions?

Upvotes: 1

Views: 585

Answers (3)

Aranxo
Aranxo

Reputation: 1183

Thanks guys for your great ideas! I finally adapted Michael Kays answer. Now its time to present my final solution:

<xsl:template match="INVOICE">
    <xsl:for-each-group select="*" group-starting-with="POSITION_DATA_1 | POSITION_DATA_2[not(preceding-sibling::POSITION_DATA_1)]">
        <xsl:choose>
            <!-- Do no wrap the header data group -->
            <xsl:when test="starts-with(local-name(), 'HEADER_DATA_1')">
                <xsl:apply-templates select="current-group()"/>
            </xsl:when>
            <xsl:otherwise>
                <POSITION>
                    <xsl:apply-templates select="current-group()"/>
                </POSITION>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:for-each-group>
</xsl:template>

Upvotes: 0

Michael Kay
Michael Kay

Reputation: 163262

If you can use XSLT 2.0 then this is achieved using positional grouping:

<xsl:template match="INVOICE">
  <xsl:for-each-group select="*" group-starting-with="POSITION_DATA_1 | POSITION_DATA_2[not(preceding-sibling::*[1][self::POSITION_DATA_1]">
    <xsl:choose>
      <xsl:when test="starts-with(local-name(), 'POSITION'>
        <POSITION>
          <xsl:copy-of select="current-group()"/>
        </POSITION>
      </xsl:when>
      <xsl:otherwise>
        <xsl:copy-of select="current-group()"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:for-each-group>
</xsl:template>

Upvotes: 0

Tim C
Tim C

Reputation: 70598

Consider using a key to group the POSITION_DATA_2 to POSITION_DATA_8 elements by the preceding POSITION_DATA_1

Then, to wrap a containing tag around all the items, you can simply have a template POSITION_DATA_1 and then use the key to get all the other items to wrap.

  <xsl:template match="POSITION_DATA_1">
    <POSITION>
      <xsl:call-template name="identity" />
      <xsl:apply-templates select="key('pos', generate-id())" />
    </POSITION>
  </xsl:template>

Try this XSLT

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="xml" indent="yes" />
  <xsl:key name="pos" 
           match="*[starts-with(local-name(), 'POSITION_DATA_') and not(self::POSITION_DATA_1)]"
           use="generate-id(preceding-sibling::POSITION_DATA_1[1])" />

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

  <xsl:template match="INVOICE">
    <xsl:copy>
        <xsl:apply-templates select="HEADER_DATA_1|HEADER_DATA_2|HEADER_DATA_3"/>
        <xsl:apply-templates select="POSITION_DATA_1"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="POSITION_DATA_1">
    <POSITION>
      <xsl:call-template name="identity" />
      <xsl:apply-templates select="key('pos', generate-id())" />
    </POSITION>
  </xsl:template>
</xsl:stylesheet>

Upvotes: 1

Related Questions