Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

XSLT to flatten XML

Tags:

xml

xslt

Could anyone please help me with the following transformation?

Here is the input xml:

<?xml version="1.0" encoding="UTF-8"?>
<book>
    <title>My book</title>
    <pages>200</pages>
    <size>big</size>
    <author>
        <name>Smith</name>
    </author>
    <author>
        <name>Wallace</name>
    </author>
    <author>
        <name>Brown</name>
    </author>
</book>
<book>
    <title>Other book</title>
    <pages>100</pages>
    <size>small</size>
    <author>King</author>
</book>
<book>
    <title>Pretty book</title>
    <pages>150</pages>
    <size>medium</size>
</book>

This is the desired output

<book style="even">
    <title>My book</title>
    <pages>200</pages>
    <size>big</size>
    <author-name>Smith</author-name>
</book>
<book style="odd">
    <title>My book</title>
    <pages>200</pages>
    <size>big</size>
    <author-name>Wallace</author-name>
</book>
<book style="even">
    <title>My book</title>
    <pages>200</pages>
    <size>big</size>
    <author-name>Brown</author-name>
</book>
<book style="odd" >
    <title>Other book</title>
    <pages>100</pages>
    <size>small</size>
    <author-name>King</author-name>
</book>
<book style="even">
    <title>Pretty book</title>
    <pages>150</pages>
    <size>medium</size>
    <author-name />
</book>

I tried using xsl:for-each loops, but I suppose they brought me to a dead end. The tricky part here is the "style" attribute that somehow needs to be "global" no matter how many author tags are placed in any book.

like image 422
Funky coder Avatar asked Feb 23 '23 19:02

Funky coder


2 Answers

This simple, pure XSLT 1.0 transformation (no conditionals, no xsl:for-each, no parameter passing, no xsl:element, no use of the infamously inefficient //):

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:my="my:my" exclude-result-prefixes="my" >
    <xsl:output omit-xml-declaration="yes" indent="yes"/>
    <xsl:strip-space elements="*"/>

    <my:names>
     <n>odd</n>
     <n>even</n>
    </my:names>

    <xsl:variable name="vStyles"
        select="document('')/*/my:names/*"/>

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

    <xsl:template match="book/author">
     <xsl:variable name="vPos">
      <xsl:number level="any" count="book/author|book[not(author)]"/>
     </xsl:variable>
        <book style="{$vStyles[$vPos mod 2 +1]}">
          <xsl:copy-of select="@*|../node()[not(self::author)]"/>
          <author-name>
            <xsl:value-of select="normalize-space()"/>
      </author-name>
        </book>
    </xsl:template>

    <xsl:template match="book[author]">
     <xsl:apply-templates/>
    </xsl:template>

    <xsl:template match="book[not(author)]">
     <xsl:variable name="vPos">
      <xsl:number level="any" count="book/author|book[not(author)]"/>
     </xsl:variable>
        <book style="{$vStyles[$vPos mod 2 +1]}">
          <xsl:copy-of select="@*|node()"/>
          <author-name/>
        </book>
    </xsl:template>

    <xsl:template match="book[author]/*[not(self::author)]"/>
</xsl:stylesheet>

when applied to this XML document (the provided one wrapped into a single top element):

<t>
    <book>
        <title>My book</title>
        <pages>200</pages>
        <size>big</size>
        <author>
            <name>Smith</name>
        </author>
        <author>
            <name>Wallace</name>
        </author>
        <author>
            <name>Brown</name>
        </author>
    </book>
    <book>
        <title>Other book</title>
        <pages>100</pages>
        <size>small</size>
        <author>King</author>
    </book>
    <book>
        <title>Pretty book</title>
        <pages>150</pages>
        <size>medium</size>
    </book>
</t>

produces exactly the wanted, correct result:

<t>
   <book style="even">
      <title>My book</title>
      <pages>200</pages>
      <size>big</size>
      <author-name>Smith</author-name>
   </book>
   <book style="odd">
      <title>My book</title>
      <pages>200</pages>
      <size>big</size>
      <author-name>Wallace</author-name>
   </book>
   <book style="even">
      <title>My book</title>
      <pages>200</pages>
      <size>big</size>
      <author-name>Brown</author-name>
   </book>
   <book style="odd">
      <title>Other book</title>
      <pages>100</pages>
      <size>small</size>
      <author-name>King</author-name>
   </book>
   <book style="even">
      <title>Pretty book</title>
      <pages>150</pages>
      <size>medium</size>
      <author-name/>
   </book>
</t>

Explanation: Appropriate use of xsl:number and templates/pattern matching.

like image 94
Dimitre Novatchev Avatar answered Feb 25 '23 07:02

Dimitre Novatchev


This stylesheet first stores all author nodes as well as all book nodes without authors in a global variable. "/." sorts them in document order. Then the main template iterates over all nodes in this variable and generates output according to the position in the sequence.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:variable name="authors" select="(//author | //book[not(author)])/."/>

  <xsl:template match="/">
    <xsl:element name="result">
      <xsl:for-each select="$authors">
        <xsl:apply-templates select=".">
          <xsl:with-param name="position" select="position()+1"/>
        </xsl:apply-templates>
      </xsl:for-each>
    </xsl:element>
  </xsl:template>

  <xsl:template match="text()|title|pages|size">
    <xsl:copy>
      <xsl:apply-templates/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="book">
    <xsl:param name="position" select="1"/>
    <xsl:element name="book">
      <xsl:attribute name="style">
        <xsl:if test="$position mod 2 = 0">even</xsl:if>
        <xsl:if test="$position mod 2 = 1">odd</xsl:if>
      </xsl:attribute>
      <xsl:apply-templates select="title"/>
      <xsl:apply-templates select="pages"/>
      <xsl:apply-templates select="size"/>
      <xsl:element name="author-name"/>
    </xsl:element>
  </xsl:template>

  <xsl:template match="author">
    <xsl:param name="position" select="1"/>
    <xsl:element name="book">
      <xsl:attribute name="style">
        <xsl:if test="$position mod 2 = 0">even</xsl:if>
        <xsl:if test="$position mod 2 = 1">odd</xsl:if>
      </xsl:attribute>
      <xsl:apply-templates select="../title"/>
      <xsl:apply-templates select="../pages"/>
      <xsl:apply-templates select="../size"/>
      <xsl:element name="author-name">
        <xsl:if test="name">
          <xsl:value-of select="name"/>
        </xsl:if>
        <xsl:if test="not(name)">
          <xsl:value-of select="."/>
        </xsl:if>
      </xsl:element>
    </xsl:element>
  </xsl:template>
</xsl:stylesheet>
like image 20
Ghislain Fourny Avatar answered Feb 25 '23 07:02

Ghislain Fourny