Basic xslt transformation - xslt

I have one XML request which I need to modify (to XML) and then send it further. I have no prior knowledge of XSLT.
Say I have
<Combined>
<Profile>
<Fullname>John Doe</Fullname>
<OtherData>
<Birthdate>1996</Birthdate>
<FavoriteBooks>
<Book>
<id>1</id>
<description>Libre1</description>
</Book>
<Book>
<id>2</id>
<description>Libre2</description>
</Book>
<Book>
<id>3</id>
<description></description>
</Book>
<Book>
<id>4</id>
<description>Libre4</description>
</Book>
</FavoriteBooks>
</OtherData>
</Profile>
<LoadedData>
<NewBirthdate>1998</NewBirthdate>
<BooksUpdate>
<Book id="1">
<BookText>Book1</BookText>
</Book>
<Book id="2">
<BookText>Book2</BookText>
</Book>
<Book id="3">
<BookText>Book3</BookText>
</Book>
<Book id="4">
<BookText>Book4</BookText>
</Book>
<Book id="5">
<BookText>Book5</BookText>
</Book>
</BooksUpdate>
</LoadedData>
And want to get
<Profile>
<Fullname>John Doe</Fullname>
<OtherData>
<Birthdate>1998</Birthdate>
<FavoriteBooks>
<Book>
<id>1</id>
<description>Libre1Book1</description>
</Book>
<Book>
<id>2</id>
<description>Libre2Book2</description>
</Book>
<Book>
<id>3</id>
<description>empty</description>
</Book>
<Book>
<id>4</id>
<description>Libre4Book4</description>
</Book>
<Book>
<id>5</id>
<description>new Book5</description>
</Book>
</FavoriteBooks>
</OtherData>
I did a pretty pathetic attempt, which obviously does not work.
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
>
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<Profile>
<xsl:apply-templates select="Combined/Profile/Fullname" />
</Profile>
<Otherdata>
<Birthdate>
<xsl:apply-templates select="Combined/LoadedData/NewBirthdate"/>
</Birthdate>
<FavoriteBooks>
<xsl:for-each select="/Combined/Profile/OtherData/FavoriteBooks/Book">
<Book>
<id>
<xsl:value-of select="id"/>
</id>
<description>
<xsl:value-of select="description"/>
<xsl:apply-templates select="/Combined/LoadedData/BooksUpdate/Book[#id='']" />
</description>
</Book>
</xsl:for-each>
</FavoriteBooks>
</Otherdata>
</xsl:template>
</xsl:stylesheet>
How can I get closer to what I want to get? Could you also advise me some book to jump start, because w3schools tutorials are useless :(

How's this?
According to the logic you described, the description would not be "empty" but rather "Book3" (empty string merged with "Book3").
<!-- root and static content -->
<xsl:template match="/">
<xsl:apply-templates select='Combined/Profile' />
</xsl:template>
<!-- identity/copy, with some tweaks -->
<xsl:template match='node()|#*'>
<!-- copy node -->
<xsl:copy>
<!-- add in its attributes -->
<xsl:apply-templates select='#*' />
<!-- now either apply same treatment to child nodes, or something special -->
<xsl:choose>
<!-- use updated birthdate -->
<xsl:when test='name() = "Birthdate"'>
<xsl:value-of select='/Combined/LoadedData/NewBirthdate' />
</xsl:when>
<!-- merge book descriptions -->
<xsl:when test='name() = "description"'>
<xsl:value-of select='concat(., /Combined/LoadedData/BooksUpdate/Book[#id = current()/../id]/BookText)' />
</xsl:when>
<!-- or just keep recursing -->
<xsl:otherwise>
<xsl:apply-templates select='node()' />
</xsl:otherwise>
</xsl:choose>
</xsl:copy>
<!-- if we've done all books, add in any in the loaded data but not the original data -->
<xsl:if test='name() = "Book" and not(count(following-sibling::Book))'>
<xsl:variable name='orig_book_ids'>
<xsl:for-each select='../Book'>
<xsl:value-of select='concat("-",id,"-")' />
</xsl:for-each>
</xsl:variable>
<xsl:apply-templates select='/Combined/LoadedData/BooksUpdate/Book[not(contains($orig_book_ids, concat("-",#id,"-")))]' mode='new_books' />
</xsl:if>
</xsl:template>
<!-- new books -->
<xsl:template match='Book' mode='new_books'>
<Book>
<id><xsl:value-of select='#id' /></id>
<description>new <xsl:value-of select='BookText' /></description>
</Book>
</xsl:template>
You can run it at this XMLPlayground session (see output source).

Related

Output only Duplicates in the final Text Output

I am trying to output only one line per unique value in my final text output after running an XML through an XSL stylesheet. In my research, I came upon the distinct-values function, but I'm unable to execute it the way that I want.
Here is my XML:
<Library>
<Book>
<Code>1</Code>
<Title>MANAGEMENT</Title>
</Book>
<Book>
<Code>1</Code>
<Title>MANAGEMENT</Title>
</Book>
<Book>
<Code>1</Code>
<Title>MANAGEMENT</Title>
</Book>
<Book>
<Code>1</Code>
<Title>MANAGEMENT</Title>
</Book>
<Book>
<Code>1</Code>
<Title>MANAGEMENT</Title>
</Book>
<Book>
<Code>10</Code>
<Title>MECHANICAL</Title>
</Book>
<Book>
<Code>106</Code>
<Title>TRANSPORTATION</Title>
</Book>
</Library>
And here is my current XSL (incorrect):
<xsl:template match="Book">
<xsl:value-of select="this:fixedOutput(Code)" />
<xsl:value-of select="this:fixedOutput(Title)" />
<xsl:value-of select="$linefeed" />
</xsl:template>
My output right now is:
1|MANAGEMENT| 1|MANAGEMENT| 1|MANAGEMENT| 1|MANAGEMENT| 1|MANAGEMENT|
10|MECHANICAL| 106|TRANSPORTATION|
But I want it to be this:
1|MANAGEMENT| 10|MECHANICAL| 106|TRANSPORTATION|
I'm not sure how to use the syntax of distinct values to get to where I need.
An XSLT 1.0 solution that uses key and the generate-id() function (Muenchian grouping) to get distinct values :
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="bookCode" match="/Library/Book/Code" use="." />
<xsl:template match="/">
<xsl:for-each select="/Library/Book/Code[generate-id()
= generate-id(key('bookCode',.)[1])]">
<xsl:value-of select="this:fixedOutput(.)" />
<xsl:value-of select="this:fixedOutput(../Title)" />
<xsl:value-of select="$linefeed" />
</xsl:for-each>
</xsl:template>
An XSLT 2.0 solution which uses xsl:for-each-group as #michael.hor257k said :
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0" xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs">
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*" />
</xsl:copy>
</xsl:template>
<xsl:template match="Library">
<xsl:for-each-group select="Book" group-by="concat(Code,Title)">
<xsl:apply-templates select="." />
</xsl:for-each-group>
</xsl:template>
<xsl:template match="Book">
<xsl:value-of select="this:fixedOutput(Code)" />
<xsl:value-of select="this:fixedOutput(Title)" />
<xsl:value-of select="$linefeed" />
</xsl:template>
Note: As this:fixedOutput in your code doen't refer any namespace it has been used as it is.
Refer this: http://xsltransform.net/3MP2uBE

How to use ends-with in xslt 2.0?

I'm trying use ends-with in xslt for return any values. for example: I've this xml:
<BOOKS>
<BOOK>
<TITLE>title1</TITLE>
<ISSN>12313213</ISSN>
</BOOK>
<BOOK>
<TITLE>title2</TITLE>
<ISSN>67895776</ISSN>
</BOOK>
<BOOK>
<TITLE>title3</TITLE>
<ISSN>54363645</ISSN>
</BOOK>
</BOOKS>
and this static xml(book.xml):
<BOOKS>
<BOOK>
<VALUE>test title12</VALUE>
<PRICE>1235,23</PRICE>
</BOOK>
<BOOK>
<VALUE>test title1</VALUE>
<PRICE>345,23</PRICE>
</BOOK>
</BOOKS>
I need to verify if there's a book title in title xml. My code:
<xsl:template match="/">
<xsl:variable name="book" select="document('file:///E:/book.xml')"/>
<BOOKS>
<xsl:for-each select="$book/books/book">
<xsl:variable name="value" select="VALUE"/>
<xsl:variable name="price" select="ESTRATO"/>
<xsl:for-each select="//BOOKS">
<xsl:for-each select="BOOK">
<xsl:if test="ends-with($value, #TITLE)">
<BOOK>
<TITLE><xsl:value-of select="#TITLE"/></TITLE>
<ISSN><xsl:value-of select="$value"/></ISSN>
<PRICE><xsl:value-of select="$price"/></PRICE>
</BOOK>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</xsl:for-each>
</BOOKS>
</xsl:template>
I wanna return when the book title ends-with in the tag VALUE in the xml books. Can anyone Help me? Thanks.
I've tried to use something like that How to use contains in xslt? but doesn't worked.
Your question is not entirely clear. I think you want to do something like this:
XSLT 2.0
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:param name="book" select="document('file:///E:/book.xml')"/>
<xsl:template match="/BOOKS">
<BOOKS>
<xsl:for-each select="BOOK">
<BOOK>
<xsl:copy-of select="TITLE | ISSN"/>
<PRICE>
<xsl:value-of select="$book/BOOKS/BOOK[ends-with(VALUE, current()/TITLE)]/PRICE"/>
</PRICE>
</BOOK>
</xsl:for-each>
</BOOKS>
</xsl:template>
</xsl:stylesheet>
Note that XML is case-sensitive: book does not match/select BOOK.

How to use contains in xslt?

I'm trying use contains in xslt for return any values.
for example:
I've this xml:
<BOOKS>
<BOOK>
<TITLE>title1</TITLE>
<ISSN>12313213</ISSN>
</BOOK>
<BOOK>
<TITLE>title2</TITLE>
<ISSN>67895776</ISSN>
</BOOK>
<BOOK>
<TITLE>title3</TITLE>
<ISSN>54363645</ISSN>
</BOOK>
</BOOKS>
and this static xml(issn.xml):
<ISSNS>
<ISSN>
<VALUE>12313213, 67895776</VALUE>
<PRICE>1235,23</PRICE>
</ISSN>
<ISSN>
<VALUE>5463432, 54363645</VALUE>
<PRICE>345,23</PRICE>
</ISSN>
</ISSNS>
I need to verify if have book issn in issn xml.
My code:
<xsl:template match="/">
<xsl:variable name="issn" select="document('file:///E:/issn.xml')"/>
<BOOKS>
<xsl:for-each select="$issn/ISSNS/ISSN">
<xsl:variable name="value" select="VALUE"/>
<xsl:variable name="price" select="ESTRATO"/>
<xsl:for-each select="//BOOKS">
<xsl:for-each select="BOOK">
<xsl:if test="contains($value, #ISSN)">
<BOOK>
<TITLE><xsl:value-of select="#TITLE"/></TITLE>
<ISSN><xsl:value-of select="$value"/></ISSN>
<PRICE><xsl:value-of select="$price"/></PRICE>
</BOOK>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</xsl:for-each>
</BOOKS>
</xsl:template>
I wanna return when the book issn contains in issn value. Can anyone Help me? Thanks
If I understand this correctly, you want to do:
XSLT 1.0
<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:param name="issns" select="document('file:///E:/issn.xml')"/>
<xsl:template match="/BOOKS">
<BOOKS>
<xsl:for-each select="BOOK">
<xsl:variable name="issn" select="$issns/ISSNS/ISSN[contains(VALUE, current()/ISSN)] "/>
<xsl:if test="$issn">
<BOOK>
<xsl:copy-of select="*"/>
<xsl:copy-of select="$issn/PRICE"/>
</BOOK>
</xsl:if>
</xsl:for-each>
</BOOKS>
</xsl:template>
</xsl:stylesheet>
Note that this assumes one ISSN value cannot contain another (which could possibly generate a false match).

Groups two xml files like a sql group-by [2]

This is an evolution of my ask here :
Groups two xml files like a sql group-by
The Example given and Dimitre Solution was counting distinct isbn value.
Now modify library xml to have
mylibrary.xml :
<library>
<book id="1" isbn="1"/>
<book id="2" isbn="1"/>
<book id="3" isbn="2"/>
<book id="4" isbn="4"/>
<book id="5" isbn="5"/>
<book id="6" isbn="4"/>
<book id="7" isbn="4"/>
</library>
and this one that can be used :
bookreference.xml :
<reference>
<book isbn="1">
<category>SF</category>
</book>
<book isbn="2">
<category>SF</category>
</book>
<book isbn="3">
<category>SF</category>
</book>
<book isbn="4">
<category>Comedy</category>
</book>
<book isbn="5">
<category>Comedy</category>
</book>
</reference>
i want to get the numbers of book i got in mylibrary 'even if some have same isbn', groupby category, using xslt 1-0.
output wanted:
SF : 3 book(s)
Comedy : 4 book(s)
my xslt propose here : Groups two xml files like a sql group-by works fine but of course use 'for-each' loop and extension functions.
Surely there is a better solution.
Again a very good question! (+1)
This transformation, using two keys for achieving full efficiency:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="kBookByCat" match="book"
use="category"/>
<xsl:key name="kBookByIsbn" match="book"
use="#isbn"/>
<xsl:variable name="vDoc" select="/"/>
<xsl:variable name="vRef" select=
"document('file:///c:/temp/delete/reference.xml')"/>
<xsl:variable name="vMyIsbns" select="/*/*/#isbn"/>
<xsl:variable name="vResult">
<xsl:apply-templates select="$vRef/*"/>
</xsl:variable>
<xsl:template match="/">
<xsl:copy-of select="$vResult"/>
</xsl:template>
<xsl:template match=
"book[generate-id()
=
generate-id(key('kBookByCat', category)[1])
]
">
<xsl:variable name="vBooksinCat" select=
"key('kBookByCat', category)"/>
<xsl:value-of select="category"/> : <xsl:text/>
<xsl:for-each select="$vDoc">
<xsl:value-of select="count(key('kBookByIsbn',$vBooksinCat/#isbn))"/>
</xsl:for-each>
<xsl:text> book(s)
</xsl:text>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
when applied on the provided XML document contained in the file mylibrary.xml:
<library>
<book id="1" isbn="1"/>
<book id="2" isbn="1"/>
<book id="3" isbn="2"/>
<book id="4" isbn="4"/>
<book id="5" isbn="5"/>
<book id="6" isbn="4"/>
<book id="7" isbn="4"/>
</library>
and having this provided XML document in C:\temp\delete\reference.xml:
<reference>
<book isbn="1">
<category>SF</category>
</book>
<book isbn="2">
<category>SF</category>
</book>
<book isbn="3">
<category>SF</category>
</book>
<book isbn="4">
<category>Comedy</category>
</book>
<book isbn="5">
<category>Comedy</category>
</book>
</reference>
produces the wanted, correct output:
SF : 3 book(s)
Comedy : 4 book(s)
modified Dimitri version to work for this
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="kBookByCat" match="book" use="category"/>
<xsl:variable name="vRef" select="document('file:///c:/temp/delete/reference.xml')"/>
<xsl:variable name="meh" select="*"/>
<xsl:template match="/">
<xsl:apply-templates select="$vRef/reference/book[generate-id()=generate-id(key('kBookByCat', category)[1])]" />
</xsl:template>
<xsl:template match="book">
<xsl:variable name="cat" select="category"/>
<xsl:value-of select="category"/> : <xsl:text/>
<xsl:variable name="isbns" select="$vRef/reference/book[category=$cat]/#isbn"/>
<xsl:value-of select="count($meh/book[#isbn=$isbns])"/>
<xsl:text> book(s)
</xsl:text>
</xsl:template>
in add to the great dimitri response, i propose the following to not print book category that have 0 book set in mylibrary :
<xsl:variable name="catname" select="category"/>
<xsl:for-each select="$vDoc">
<xsl:variable name="cnt" select="count(key('kBookByIsbn',$vBooksinCat/#isbn))"/>
<xsl:if test="$cnt > 0">
<xsl:value-of select="$catname"/> :
<xsl:text/>
<xsl:value-of select="$cnt"/>
<xsl:text> book(s)
</xsl:text>
</xsl:if>
</xsl:for-each>

Grouping when using 2 different XML files as sources?

what i want to do is a typical grouping that can usualy be done using xsl:key, but it become more complicated as data to groups are in 2 differents files. How process ?
Here is an example of what i want to do, can i request your help ? must be xslt-1.0 compliant.
bookreference.xml :
<t>
<book isbn="1">
<category>SF</category>
</book>
<book isbn="2">
<category>SF</category>
</book>
<book isbn="3">
<category>SF</category>
</book>
<book isbn="4">
<category>Comedy</category>
</book>
<book isbn="5">
<category>Comedy</category>
</book>
</t>
mylibrary.xml :
<t>
<book isbn="1">
<price>10</price>
</book>
<book isbn="2">
<price>10</price>
</book>
<book isbn="3">
<price>20</price>
</book>
<book isbn="4">
<price>5</price>
</book>
</t>
output wanted:
SF : 3 book(s) - Total : 40$
Comedy : 2 book(s) - Total : 5$
As already suggested in my comment, you can first merge two documents into a result tree fragment, then use exsl:node-set to get a node-set on which you can then apply Muenchian grouping:
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
exclude-result-prefixes="exsl"
version="1.0">
<xsl:param name="price-url" select="'test2011113002.xml'"/>
<xsl:variable name="doc2" select="document($price-url)"/>
<xsl:output method="text"/>
<xsl:variable name="rtf">
<xsl:apply-templates select="//book" mode="merge"/>
</xsl:variable>
<xsl:template match="book" mode="merge">
<xsl:copy>
<xsl:variable name="isbn" select="#isbn"/>
<xsl:copy-of select="#* | node()"/>
<xsl:for-each select="$doc2">
<xsl:copy-of select="key('k1', $isbn)/price"/>
</xsl:for-each>
</xsl:copy>
</xsl:template>
<xsl:key name="k1" match="book" use="#isbn"/>
<xsl:key name="k2" match="book" use="category"/>
<xsl:template match="/">
<xsl:apply-templates select="exsl:node-set($rtf)/book[generate-id() = generate-id(key('k2', category)[1])]"/>
</xsl:template>
<xsl:template match="book">
<xsl:variable name="current-group" select="key('k2', category)"/>
<xsl:value-of select="concat($current-group/category, ': ', count($current-group), ' - Total : ', sum($current-group/price), '
')"/>
</xsl:template>
</xsl:stylesheet>
Good question, +1.
There is no need for complicated (more than necessary) grouping or for any extension functions:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="kBookByCat" match="book"
use="category"/>
<xsl:key name="kPriceByIsbn" match="price"
use="../#isbn"/>
<xsl:variable name="vMyLib" select=
"document('file:///c:/temp/delete/mylibrary.xml')"/>
<xsl:template match=
"book[generate-id()
=
generate-id(key('kBookByCat', category)[1])
]
">
<xsl:variable name="vBooksinCat" select=
"key('kBookByCat', category)"/>
<xsl:value-of select="category"/> : <xsl:text/>
<xsl:value-of select="count($vBooksinCat)"/>
<xsl:text> book(s) - Total : $</xsl:text>
<xsl:for-each select="$vMyLib">
<xsl:value-of select="sum(key('kPriceByIsbn', $vBooksinCat/#isbn))"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
When this transformation is applied on the first of the two provided data fragments (corrected to well-formed XML document):
<t>
<book isbn="1">
<category>SF</category>
</book>
<book isbn="2">
<category>SF</category>
</book>
<book isbn="3">
<category>SF</category>
</book>
<book isbn="4">
<category>Comedy</category>
</book>
<book isbn="5">
<category>Comedy</category>
</book>
</t>
and having the second data fragment (corrected to well-formed XML document) saved as C:\Temp\Delete\mylibrary.xml,
The wanted, correct result is produced:
SF : 3 book(s) - Total : $40
Comedy : 2 book(s) - Total : $5