comparing element using XSLT dynamically - xslt

I have XML document something like below.
<root>
<Book>
<Book_title/>
<author_name/>
</Book>
<Book>
<Book_title/>
<author_name/>
</Book>
<author_details>
<author_name/>
<author_DOB/>
<author_details/>
</root>
So can we compare Book/author_name with author_details/author_name dynamically with XSLT ...??

Define a key
<xsl:key name="author" match="author_details" use="autor_name"/>
then write a template for
<xsl:template match="Book/author_name">
<xsl:copy-of select=". | key('author', .)/author_DOB"/>
</xsl:template>
handle root and Book by the identity transformation (e.g. <xsl:mode on-no-match="shallow-copy"/> in XSLT 3) and add an empty
<xsl:template match="author_details"/>
to prevent copying/outputting those elements.

Related

XSL: how to use document() to read xml files in a folder

I need to compare xml files from two folders and collect those xml elements that only show up in one of the xml file.
The xml files in two folder has same file name.
Below is the sample of what I want to do:
old/booklist1.xml
<books>
<book #type="fiction">
<isn>12345678</isn>
<name>xxxx</name>
</book>
</books>
new/booklist1.xml
<books>
<book #type="fiction">
<isn>12345678</isn>
<name>xxxx</name>
</book>
<book #type="history">
<isn>23456789</isn>
<name>yyyyy</name>
</book>
</books>
I will need the output of the booklist1.xml as the below:
<books>
<book #type="history">
<isn>23456789</isn>
<name>yyyyy</name>
</book>
</books>
I have below findDiff.xsl that works when I specify / hardcode the xml file name:
<xsl:key name="book" match="book" use="." />
<xsl:template match="/books">
<xsl:copy>
<xsl:copy-of select="book[not(key('book', ., document('old_booklist1.xml')))]"/>
</xsl:copy>
</xsl:template>
The fidDiff.xsl current is associated with new/booklist1.xml and I copied the old/booklist1.xml to the same folder with new/booklist1.xml and made the name as old_booklist1.xml and above xsl works with the hard coded uri.
I have to loop throw xml file in folder new and then compare it with the same named xml file in folder old.
I am thinking to use the following way to build the xml file URI:
loop in the new and get the file uri
build the file uri for xml file in old folder
<xsl:variable name="xmlPath" select="document-uri()"/>
<xsl:variable name="compareWithPath" select=" replace($xmlFilePath, 'new', 'old')"/>
then pass the compareWithPath to below template:
<xsl:template match="/books">
<xsl:copy>
<xsl:copy-of select="book[not(key('book',., document($compareWithPath)))]"></xsl:copy-of>
</xsl:copy>
</xsl:template>
But I got the error that The system cannot find the file specified
file:/C:/Users/phyllis/Documents/old/booklist1.xml
Michael Kay mentioned that we can convert the file name to URI and use doc() or document() to load it. I build the filename URI exactly the same way that I got from document-uri(). What am I wrong here?
The converted file URI looks like this:
<compareWithPath>file:/C:/Users/phyllis/Documents/old/booklist1.xml</compareWithPath>
Returns false when check above file URI using:
<fileExist><xsl:value-of select="doc-available($compareWithPath)"/></fileExist>
The below xsl code works well for my problem:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="xs" version="3.0">
<xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>
<xsl:key name="book" match="book" composite="yes" use="*"/>
<xsl:template match="books">
<xsl:param name="compareWith"/>
<xsl:copy>
<xsl:copy-of
select="book[not(key('book', *, document($compareWith)))]"/>
</xsl:copy>
</xsl:template>
<xsl:template match="/">
<xsl:copy>
<xsl:iterate select="uri-collection('new') ! doc(.)">
<xsl:variable name="fileUri" select=" concat('update/', tokenize( document-uri(.),'/')[last()])"/>
<xsl:result-document method="xml" href="{$fileUri}">
<xsl:apply-templates select="books">
<xsl:with-param name="compareWith" select="concat('old/', tokenize( document-uri(.),'/')[last()])"/>
</xsl:apply-templates>
</xsl:result-document>
</xsl:iterate>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Only a key() is able to eliminate the duplicates, find diff, compare file, etc. Never thought it could be so easy to solve this kind of problem using xsl but once and once xsl proved his power when it comes to xml.
<xsl:iterate select="uri-collection() ! doc(.)">...</xsl:iterate> iterate through uri-collection() makes it easy to loop through the folders.

Get the last last value of a node

I am trying to get the last value of a node that belongs to a group.
Given the following XML -
<books>
<author>
<name>R.R.</name>
<titles>
<titlename>North</titlename>
<titlename>King</titilename>
</titles>
</author>
<author>
<name>R.L.</name>
<titles>
<titlename>Dragon</titlename>
<titlename>Wild</titilename>
</titles>
</author>
</books>
I assume it would be something like -
<template match="/">
<for-each-group select="books/author" group-by="name">
<lastTitle>
<name><xsl:value-of select="name"/></name>
<title><xsl:value-of select="last()/name"/></title>
</lastTitle
</for-each-group>
<template>
Thus the result would be -
<lastTitle>
<name>R.R</name>
<title>King</title>
</lastTitle>
<lastTitle>
<name>R.L.</name>
<title>Wild</title>
</lastTitle>
How can I produce this result?
Instead of doing this
<xsl:value-of select="last()/name"/>
The expression you are looking for is this:
<xsl:value-of select="titles/titlename[last()]"/>
However, it might be worth pointing out, this isn't really a 'grouping' problem. (It would only be a grouping problems if you have two different author elements with the same name). You can actually just use a simple xsl:for-each here
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<xsl:for-each select="books/author">
<lastTitle>
<name>
<xsl:value-of select="name"/>
</name>
<title>
<xsl:value-of select="titles/titlename[last()]"/>
</title>
</lastTitle>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

XSLT: Using variables in a key function

I have a following XML file:
<titles>
<book title="XML Today" author="David Perry"/>
<book title="XML and Microsoft" author="David Perry"/>
<book title="XML Productivity" author="Jim Kim"/>
<book title="XSLT 1.0" author="Albert Jones"/>
<book title="XSLT 2.0" author="Albert Jones"/>
<book title="XSLT Manual" author="Jane Doe"/>
</titles>
I want to eliminate some elements and apply the following XSLT:
<?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:key name="author1-search" match="book[starts-with(#author, 'David')]" use="#title"/>
<xsl:template match="book [key('author1-search', #title)]" />
<xsl:key name="author2-search" match="book[starts-with(#author, 'Jim')]" use="#title"/>
<xsl:template match="book [key('author2-search', #title)]" />
<xsl:template match="/">
<xsl:apply-templates />
</xsl:template>
</xsl:stylesheet>
Is it possible to use an inline xsl variable
<xsl:variable name="Author">
<name>David</name>
<name>Jim</name>
</xsl:variable>
instead of "author1-search", "author2-search", and so on to loop through names?
I can use only XSLT 1.0 (2.0 is currently not supported).
Thanks in advance,
Leo
No, patterns (in XSLT 1.0) cannot contain variable/parameter references.
One way to perform such a task would be like this:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:param name="pAuthor" select="'David Perry'"/>
<xsl:key name="kBookaByAuthor" match="book"
use="#author"/>
<xsl:template match="/">
Books written by <xsl:value-of select="$pAuthor"/> :<xsl:text/>
<xsl:apply-templates
select="key('kBookaByAuthor', $pAuthor)"/>
</xsl:template>
<xsl:template match="book">
<!-- One can do more formatting here -->
<xsl:text>
</xsl:text>
<xsl:value-of select="#title"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<titles>
<book title="XML Today" author="David Perry"/>
<book title="XML and Microsoft" author="David Perry"/>
<book title="XML Productivity" author="Jim Kim"/>
<book title="XSLT 1.0" author="Albert Jones"/>
<book title="XSLT 2.0" author="Albert Jones"/>
<book title="XSLT Manual" author="Jane Doe"/>
</titles>
the wanted, correct result is produced:
Books written by David Perry :
XML Today
XML and Microsoft
Update: In a comment the OP has clarified that:
"I thought I fully specified my requirements in the initial question.
As I mentioned in my question and in my first comment, it would be
helpful to me to see the approach for dealing with more than one
author"
Here is a solution that truly uses keys (note that the "key" in the answer by #Flynn1179 doesn't build any index and is just a constant sequence of strings-- so the function key() using that xsl:key is actually finding a string in a list of strings -- which is O(N) as contrasted to, typically, O(1) for searching in a true index):
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common">
<xsl:output method="text"/>
<xsl:param name="pAuthors">
<x>David Perry</x>
<x>Jane Doe</x>
</xsl:param>
<xsl:variable name="vParams" select=
"ext:node-set($pAuthors)/*"/>
<xsl:key name="kBookByAuthor" match="book"
use="#author"/>
<xsl:template match="/">
Books written by : <xsl:text/>
<xsl:apply-templates select="$vParams"/>
<xsl:apply-templates select=
"key('kBookByAuthor', $vParams)"/>
</xsl:template>
<xsl:template match="book">
<!-- One can do more formatting here -->
<xsl:text>
</xsl:text>
<xsl:value-of select="concat('"', #title, '"')"/>
</xsl:template>
<xsl:template match="x">
<xsl:if test="not(position() = 1)">, </xsl:if>
<xsl:value-of select="."/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied to the provided XML document (above), the wanted, correct result is produced:
Books written by : David Perry, Jane Doe
"XML Today"
"XML and Microsoft"
"XSLT Manual"
Do note: In this solution the Exslt function node-set() is used. This is done only for convenience here. In a real usage, the value of the parameter will be specified externally and then the ext:node-set() function isn't necessary.
Efficiency: This solution uses the true power of keys in XSLT. An experiment made using MSXML (3, 4 and 6) XSLT processors shows that if we search for 10000 authors the transformation time with different XSLT processors ranges from: 32ms to 45ms.
Interestingly, the solution presented by #Flynn1179 doesn't indeed make key index and with many XSLT processors it takes (for the same number (10000) of authors) from 1044ms to 5564ms:
MSXML3: 5564 ms.,
MSXML4: 2526ms,
MSXML6: 4867 ms,
AltovaXML: 1044ms.
This is quite inferior to the performance one gets with true key indexing (32ms to 45ms).
Patterns in XSLT 1.0 are not allowed to contain variable or parameter references so you couldn't use variable or parameter references in those key definitions or in the template match attributes you have.
Rather than using variables, you could just include an element in your XSLT sheet in it's own namespace, and refer to that, like this:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:my="my:my">
<xsl:key name="authors" use="document('')/*/my:authors/my:name" match="/" />
<my:authors>
<my:name>David Perry</my:name>
<my:name>Jim Kim</my:name>
</my:authors>
<xsl:template match="book[not(key('authors',#author))]" />
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
The book template matches those that do NOT have a corresponding my:name element for their author, and outputs nothing. The identity template outputs everything else, including the book elements you DO care about. The key's a bit of a hack, it essentially matches the whole document where the name exists, rather than matching the my:name element that matches. Since you only care about it's existence, this shouldn't be a problem.
Alternatively, if you'd rather be able to pass in a list of authors, you can use this:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:param name="authors" select="'David Perry,Jim Kim'" />
<xsl:template match="book">
<xsl:if test="contains(concat(',',$authors,','),concat(',',#author,','))">
<xsl:call-template name="identity" />
</xsl:if>
</xsl:template>
<xsl:template match="#* | node()" name="identity">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
The variable's used in an <xsl:if> rather than in the template match, but it does the same job. This particular code needs the list of authors specified as a comma separated list, but it should be easy enough to adapt it if you'd rather use a different format.

Overriding match="*" template from DocBook XSL

DocBook XSL includes a template that matches all element
<xsl:template match="*">
<xsl:message> .... </xsl:message>
</xsl:template>
I need to override it with another template because my source XML tree contains more that just the DoocBook XML. If I specify such a template in the file it overrides all templates in DocBook XSL. It seems like that all imported templates, are prioritized on the order of import only, and NOT according to how specific the template is.
<?xml version='1.0'?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:db="http://docbook.org/ns/docbook" version="1.0">
<xsl:import href="docbook-xsl-ns/xhtml/docbook.xsl" />
<xsl:import href="copy.xsl"/>
<xsl:template match="/">
<xsl:apply-templates select="//db:book"/>
</xsl:template>
</xsl:stylesheet>
copy.xsl
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform>
<xsl:template match="*">
<xsl:element name="{local-name()}">
<!-- go process attributes and children -->
<xsl:apply-templates select="#*|node()" />
</xsl:element>
</xsl:template>
</xsl:stylesheet>
Sample XML source
<?xml version="1.0" encoding="UTF-8"?>
<root>
<http-host>localhost</http-host>
<book xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xi="http://www.w3.org/2001/XInclude" xmlns:svg="http://www.w3.org/2000/svg" xmlns:m="http://www.w3.org/1998/Math/MathML" xml:id="course.528" xml:lang="en" version="5.0">
<info>
<title>Postoperative Complications</title>
</info>
<chapter xml:id="chapter.1">
<title>INTRODUCTION</title>
<para>Postoperative complications are a constant threat to the millions ....</para>
</chapter>
</book>
<errors></errors>
</root>
This is true for both Xalan and xsltproc processors. How do I override this template without having to change the DocBook XSL source. I tried messing with priorities but that did not work.
From what I understand, you want to apply the copy.xsl's template only for non-docbook elements. Try to be more specific in your copy.xsl - by being more specific in your copy.xsl, that template will get selected for all non-docbook elements.
copy.xsl
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform>
<xsl:template match="*[not(namespace-uri() = 'http://docbook.org/ns/docbook')]">
<xsl:element name="{local-name()}">
<!-- go process attributes and children -->
<xsl:apply-templates select="#*|node()" />
</xsl:element>
</xsl:template>
</xsl:stylesheet>
Depending on the presence of DocBook elements within non-Docbook nodes, you might need to restrict the nodeset for which you apply at the apply-templates part as well(based on the namespace) and maybe mess around the apply-templates flow to make sure it handles it predictably. Hope this is of some use to you..

How do I Filter an XML via an XSLT with xml params

Here is my input XML:
<Books>
<Book>
<BookId>1</BookId>
<Des>Dumm1</Des>
<Comments/>
<OrderDateTime>04/06/2009 12:37</OrderDateTime>
</Book>
<Book>
<BookId>2</BookId>
<Des>Dummy2</Des>
<Comments/>
<OrderDateTime>04/07/2009 12:37</OrderDateTime>
</Book>
<Book>
<BookId>3</BookId>
<Des>Dumm12</Des>
<Comments/>
<OrderDateTime>05/06/2009 12:37</OrderDateTime>
</Book>
<Book>
<BookId>4</BookId>
<Des>Dummy2</Des>
<Comments/>
<OrderDateTime>06/07/2009 12:37</OrderDateTime>
</Book>
</Books>
I pass an XML param and my Input XML is
<BookIDs>
<BookID>2</BookID>
<BookID>3</BookID>
</BookIDs>
My output should be like
<Books>
<Book>
<BookId>2</BookId>
<Des>Dummy2</Des>
<Comments/>
<OrderDateTime>04/07/2009 12:37</OrderDateTime>
</Book>
<Book>
<BookId>3</BookId>
<Des>Dumm12</Des>
<Comments/>
<OrderDateTime>05/06/2009 12:37</OrderDateTime>
</Book>
</Books>
How do I accomplish this using XSLT?
This works in Saxon 6.5.5...
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.1">
<xsl:param name="nodeset">
<BookIDs><BookID>2</BookID><BookID>3</BookID></BookIDs>
</xsl:param>
<xsl:template match="/Books">
<Books>
<xsl:variable name="Copy">
<wrap>
<xsl:copy-of select="Book"/>
</wrap>
</xsl:variable>
<xsl:for-each select="$nodeset/BookIDs/BookID">
<xsl:copy-of select="$Copy/wrap/Book[BookId=current()]"/>
</xsl:for-each>
</Books>
</xsl:template>
</xsl:stylesheet>
A pure XSLT solution will be pretty brittle though. Sub-query predicates didn't work, neither did a key. It is dependent upon the param being recognized as a node-set--which I was unable to achieve with a dynamic value (as opposed to the default in my example), even with exsl:node-set. This is also wasteful in that it copies all the Book elements from the source document.
There may be a better solution in XSLT 2.0. Alternately, if you are initiating your transform with some other language/tool, there may be better approaches available there. Another possibility could include the use of exsl:document to load your source document or params.