Generic XSLT to do XML to CSV - almost there, but stuck - xslt

I have gathered bits and pieces of this XSLT from these forums. I'm trying to put them altogether to create a single, generic XSLT that can be used to convert XML to CSV by specifying the path to the nodes that should be included in the CSV file.
I have three things that I still can't figure out after about 10 hours of messing with it.
I want to iterate over each column named in csv:columns. During each iteration, I need to extract and store the text() of the column. I think this is the way to iterate, but want to make sure:
<xsl:for-each select="document('')/*/csv:columns/*">
Once I have the text() from the column, I need to put that into the columnname variable in such a way that it works when it is used with getNodeValue.
I was unable to set columnname using variable. If I didn't hard-code the value (surrounded by apostrophes), I could not get it to work. This is why I have the following line in the code:
<xsl:variable name="columnname" select="'location/city'" />
I want to pass the result of getNodeValue into quotevalue so that the result is properly quoted.
The XSLT:
<?xml version="1.0"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:csv="csv:csv" xpath-default-namespace="http://nowhere/" >
<xsl:output method="text" encoding="utf-8" />
<xsl:strip-space elements="*" />
<xsl:variable name="delimiter" select="','" />
<csv:columns>
<column>title</column>
<column>location/city</column>
</csv:columns>
<xsl:template match="job">
<xsl:value-of select="concat(#id, ',')"/>
<!-- #1 I WANT TO LOOP THROUGH ALL OF THE CSV COLUMNS HERE -->
<!-- #2 How do I put the text into the variable 'columnname' variable so that it works with getNodeValue? -->
<xsl:variable name="columnname" select="'location/city'" />
<xsl:variable name="vXpathExpression" select="$columnname"/>
<xsl:call-template name="getNodeValue">
<xsl:with-param name="pExpression" select="$vXpathExpression"/>
</xsl:call-template>
<!-- #3 After getNodeValue gets the value, I want to send that value into 'quotevalue' -->
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template name="getNodeValue">
<xsl:param name="pExpression"/>
<xsl:param name="pCurrentNode" select="."/>
<xsl:choose>
<xsl:when test="not(contains($pExpression, '/'))">
<xsl:value-of select="$pCurrentNode/*[name()=$pExpression]"/>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="getNodeValue">
<xsl:with-param name="pExpression"
select="substring-after($pExpression, '/')"/>
<xsl:with-param name="pCurrentNode" select=
"$pCurrentNode/*[name()=substring-before($pExpression, '/')]"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="quotevalue">
<xsl:param name="value"/>
<xsl:choose>
<!-- Quote the value if required -->
<xsl:when test="contains($value, '"')">
<xsl:variable name="x" select="replace($value, '"', '""')"/>
<xsl:value-of select="concat('"', $x, '"')"/>
</xsl:when>
<xsl:when test="contains($value, $delimiter)">
<xsl:value-of select="concat('"', $value, '"')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$value"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Sample XML
<?xml version="1.0" encoding="utf-8"?>
<positionfeed
xmlns="http://nowhere/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
version="2006-04">
<job id="2830302">
<employer>Acme</employer>
<title>Manager</title>
<description>Full time</description>
<postingdate>2016-09-15T23:12:13Z</postingdate>
<location>
<city>Los Angeles</city>
<state>California</state>
</location>
</job>
<job id="2830303">
<employer>Acme</employer>
<title>Clerk, evenings</title>
<description>Part time</description>
<postingdate>2016-09-15T23:12:13Z</postingdate>
<location>
<city>Albany</city>
<state>New York</state>
</location>
</job>
</positionfeed>
The current output using the XSLT I provided
2830302,Los Angeles
2830303,Albany
The output if the XSLT works as desired
2830302,Manager,Los Angeles
2830303,"Clerk, evenings",Albany
Solution (many thanks to Tim's help below)
<?xml version="1.0"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:csv="csv:csv" xpath-default-namespace="http://www.job-search-engine.com/add-jobs/positionfeed-namespace/" >
<xsl:output method="text" encoding="utf-8" />
<xsl:strip-space elements="*" />
<!-- Set the value of the delimiter character -->
<xsl:variable name="delimiter" select="','" />
<!-- The name of the node that contains the column values -->
<xsl:param name="containerNodeName" select="'job'"/>
<!-- All nodes that should be ignored during processing -->
<xsl:template match="source|feeddate"/>
<!-- The names of the nodes to be included in the CSV file -->
<xsl:variable name="columns" as="element()*">
<column header="Title">title</column>
<column header="Category">category</column>
<column header="Description">description</column>
<column header="PostingDate">postingdate</column>
<column header="URL">joburl</column>
<column header="City">location/city</column>
<column header="State">location/state</column>
</xsl:variable>
<!-- ************** DO NOT TOUCH BELOW **************** -->
<!-- ************** DO NOT TOUCH BELOW **************** -->
<!-- ************** DO NOT TOUCH BELOW **************** -->
<!-- ************** DO NOT TOUCH BELOW **************** -->
<!-- ************** DO NOT TOUCH BELOW **************** -->
<!-- Warn about unmatched nodes -->
<xsl:template match="*">
<xsl:message terminate="no">
<xsl:text>WARNING: Unmatched element: </xsl:text>
<xsl:value-of select="name()"/>
</xsl:message>
<xsl:apply-templates/>
</xsl:template>
<!-- Generate the column headers -->
<xsl:template match="//*[*[local-name()=$containerNodeName]]">
<xsl:value-of select="'Id'"/>
<xsl:value-of select="$delimiter"/>
<xsl:for-each select="$columns/#header">
<xsl:variable name="colname" select="." />
<xsl:value-of select="$colname"/>
<xsl:if test="position() != last()">
<xsl:value-of select="$delimiter"/>
</xsl:if>
</xsl:for-each>
<xsl:text>
</xsl:text>
<xsl:apply-templates />
</xsl:template>
<!-- Generate the rows of column data -->
<xsl:template match="//*[local-name()=$containerNodeName]">
<!-- TODO: Handle attributes generically -->
<xsl:value-of select="#id"/>
<xsl:variable name="container" select="." />
<xsl:for-each select="$columns">
<xsl:value-of select="$delimiter"/>
<xsl:variable name="vXpathExpression" select="."/>
<xsl:call-template name="getQuotedNodeValue">
<xsl:with-param name="pCurrentNode" select="$container"/>
<xsl:with-param name="pExpression" select="$vXpathExpression"/>
</xsl:call-template>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template name="getQuotedNodeValue">
<xsl:param name="pExpression"/>
<xsl:param name="pCurrentNode" select="."/>
<xsl:choose>
<xsl:when test="not(contains($pExpression, '/'))">
<xsl:variable name="result" select="$pCurrentNode/*[name()=$pExpression]"/>
<xsl:call-template name="quotevalue">
<xsl:with-param name="value" select="$result"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="getQuotedNodeValue">
<xsl:with-param name="pExpression" select="substring-after($pExpression, '/')"/>
<xsl:with-param name="pCurrentNode" select= "$pCurrentNode/*[name()=substring-before($pExpression, '/')]"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="quotevalue">
<xsl:param name="value"/>
<xsl:choose>
<xsl:when test="contains($value, '"')">
<!-- Quote the value and escape the double-quotes -->
<xsl:variable name="x" select="replace($value, '"', '""')"/>
<xsl:value-of select="concat('"', $x, '"')"/>
</xsl:when>
<xsl:otherwise>
<!-- Quote the value -->
<xsl:value-of select="concat('"', $value, '"')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Sample data to demonstrate solution
<?xml version="1.0" encoding="utf-8"?>
<positionfeed
xmlns="http://www.job-search-engine.com/add-jobs/positionfeed-namespace/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.job-search-engine.com/add-jobs/positionfeed-namespace/ http://www.job-search-engine.com/add-jobs/positionfeed.xsd"
version="2006-04">
<source>Casting360</source>
<feeddate>2016-11-11T21:48:34Z</feeddate><job id="1363612">
<employer>Casting360</employer>
<title>The Robert Irvine Show Is Seeking Guests</title>
<category>Reality TV</category>
<description>TV personality ROBERT IRVINE (Restaurant Impossible) is seeking guests looking for solutions to their unique problems to share their stories on his show!
Our next show is Thursday, September 22nd in LA. If you're not in LA we will provide your airfare, hotel, car service, and per diem.
Please note: WE ARE NOT LOOKING FOR RESUMES; THIS IS NOT AN ACTING GIG. We are looking for real people to share their stories!
*appearance fee (TBD)
If you or someone you know has a conflict that they need help resolving, WE WANT TO HEAR FROM YOU.
Please email tvgal.ri#gmail.com the following information:
Name
Phone number
Your story in 2-3 paragraphs
1-3 photos of yourself.</description>
<postingdate>2016-09-15T23:12:13Z</postingdate>
<joburl>http://casting360.com/lgj/8886644624?jobid=1363612&city=Los+Angeles&state=CA</joburl>
<location>
<nation>USA</nation>
<city>Los Angeles</city>
<state>California</state>
</location>
<jobsource>Casting360</jobsource>
</job><job id="1370302">
<employer>Casting360</employer>
<title>Photoshoot for Publication</title>
<category>Modeling</category>
<description>6 FEMALE Models are wanted for publication photoshoot.
If you're not in the NYC Vicinity (NY, Pa, Ct,) DO NOT REPLY because your response will be summarily ignored.
Chosen models will be given a 5 look photo shoot. The shoot will occur on location (outdoors) in highly public locations chosen both for it's convenience and scenery.
The 5 looks (outfits) will be pre-determined by our staff of items most outfits within a model's wardrobe.
THIS IS A TF (UNPAID) SHOOT. After the release of the magazine, the photos agreed upon from the shoot shall be given to the model (in digital format) for her to build her portfolio.
Chosen models will receive a 5 outfit photo shoot at no cost to them by a NY Fashion Photographer.As a result, chosen models not only receive a free photo shoot, but also become PUBLISHED MODELS featured in a magazine.
The model (Janeykay) centered in the photo attached (Please look at the attached photo) is a Casting360 member who not only received her photo shoot, not only is being featured in a magazine, but also made the cover becoming a Cover Model from her shoot with us.</description>
<postingdate>2016-10-03T00:34:43Z</postingdate>
<joburl>http://casting360.com/lgj/8886644624?jobid=1370302&city=New+York&state=NY</joburl>
<location>
<nation>USA</nation>
<city>New York</city>
<state>New York</state>
</location>
<jobsource>Casting360</jobsource>
</job><job id="1370962">
<employer>Casting360</employer>
<title>Actresses Needed for "Red Shore", Action Film</title>
<category>Acting</category>
<description>CASTING (non-union)
We are a New Independent company looking to shoot our first feature. We are currently looking to fill two Major roles.
Female/African American, Hispanic, Asian, Pacific Islander/ 5'5-5'10/ Age Late 30's-Early 40's.
Project description: A long standing feud between two best friends turned enemies escalates over a valuable Diamond on display in a New York City Museum. With the stakes high they each seek the help of both friends and strangers to settle their feud once and for all.
Please note this is a non-paid project.
Fight training will be provided for free.
Please email including age and height in your e-mail.
Those selected will be invited to our audition.</description>
<postingdate>2016-10-03T14:18:20Z</postingdate>
<joburl>http://casting360.com/lgj/8886644624?jobid=1370962&city=New+York&state=NY</joburl>
<location>
<nation>USA</nation>
<city>New York</city>
<state>New York</state>
</location>
<jobsource>Casting360</jobsource>
</job>
</positionfeed>

As you are using XSLT 2.0, you could define your columns in a variable like so:
<xsl:variable name="columns" as="element()*">
<column>title</column>
<column>location/city</column>
</xsl:variable>
Then you can just iterate over them with a simple statement
<xsl:for-each select="$columns">
But the problem you may be having is that within this xsl:for-each you have changed context. You are no longer positioned on a job element, but the column element, and you don't want your expression to be relative to that. You really need to swap back to being on the job element, which you can do simply by setting a variable reference to the job element before the xsl:for-each and then using that as a parameter to the named template:
<xsl:template match="job">
<xsl:value-of select="#id"/>
<xsl:variable name="job" select="." />
<xsl:for-each select="$columns">
<xsl:value-of select="$delimiter"/>
<xsl:variable name="vXpathExpression" select="."/>
<xsl:call-template name="getNodeValue">
<xsl:with-param name="pCurrentNode" select="$job"/>
<xsl:with-param name="pExpression" select="$vXpathExpression"/>
</xsl:call-template>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:template>
As for quoting the result; instead of doing just xsl:value-of simply call the quote template with the value as a parameter
<xsl:when test="not(contains($pExpression, '/'))">
<xsl:call-template name="quotevalue">
<xsl:with-param name="value" select="$pCurrentNode/*[name()=$pExpression]" />
</xsl:call-template>
</xsl:when>
EDIT: If you want a header row of column names, you would have to match the parent of the job node, and then just output the values of the $column variable
<xsl:template match="*[job]">
<xsl:value-of select="$columns" separator="," />
<xsl:text>
</xsl:text>
<xsl:apply-templates />
</xsl:template>
Or maybe this if you didn't want the full path
<xsl:value-of select="$columns/(tokenize(., '/')[last()])" separator="," />
Or you could extend your columns variable to have the header text
<xsl:variable name="columns" as="element()*">
<column header="Title">title</column>
<column header="City">location/city</column>
</xsl:variable>
Then you would do this...
<xsl:value-of select="$columns/#header" separator="," />

Related

Getting internal error while evaluating template - xslt

I have an issue while converting xml into csv. I'm getting a strange issue and need help.
This is my XML and XSLT, I'm getting the below error while transforming
"java.lang.RuntimeException: Internal error evaluating template at line 47 in module " that's at the line with content " xsl:template match="UsrOrder"" - bolded text
Tried but unable to figure out the issue.
XML
Below is the XML input I'm giving to XSLT and this has to be converted to CSV
<?xml version="1.0" encoding="UTF-8"?>
<document>
<businessobjects>
<UsrOrder>
<PlanOn>228.01</PlanOn>
<PROG/>
<FUND/>
<ORGN/>
<ACCT/>
<Buyer/>
<Delivery_Notes/>
<Supplier/>
<Line>1</Line>
<Item_Name>NewAir AC-12000CF Airco filter</Item_Name>
<Unit_of_Measure>STK</Unit_of_Measure>
<Order_Date/>
<Retrofit/>
<Description/>
<Product_Code/>
<Related_Invoice_Number/>
<Order_Attachments/>
<Quantity>1.0000</Quantity>
<Unit_Price>9.98</Unit_Price>
</UsrOrder>
</businessobjects>
</document>
```
XSLT
====
<?xml version="1.0"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:csv="csv:csv">
<xsl:output method="text" encoding="utf-8" omit-xml-declaration="yes" />
<xsl:strip-space elements="*" />
<xsl:variable name="delimiter" select="','" />
<csv:columns>
<column>Supplier</column>
<column>FUND</column>
<column>ORGN</column>
<column>ACCT</column>
<column>PROG</column>
<column>PlanOn</column>
<column>Delivery_Notes</column>
<column>Buyer</column>
<column>Line</column>
<column>ItemName</column>
<column>Quantity</column>
<column>Unit_Price</column>
<column>Unit_of_Measure</column>
<column>Description</column>
<column>Product_Code</column>
<column>Category</column>
<column>Retrofit</column>
<column>Related_Invoice_Number</column>
<column>OrderDate</column>
<column>OrderAttachments</column>
</csv:columns>
<xsl:template match="/document/businessobjects">
<!-- Output the CSV header -->
<xsl:for-each select="document('')/*/csv:columns/*">
<xsl:value-of select="."/>
<xsl:if test="position() != last()">
<xsl:value-of select="$delimiter"/>
</xsl:if>
</xsl:for-each>
<xsl:text>
</xsl:text>
<!-- Output rows for each matched property -->
<xsl:apply-templates select="UsrOrder" />
</xsl:template>
**<xsl:template match="UsrOrder">**
<xsl:variable name="OrderOrderLines" select="." />
<!-- Loop through the columns in order -->
<xsl:for-each select="document('')/*/csv:columns/*">
<!-- Extract the column name and value -->
<xsl:variable name="column" select="." />
<xsl:variable name="value" select="$OrderOrderLines/*[name() = $column]" />
<xsl:value-of select="$value"/>
<!-- Quote the value if required -->
<!-- <xsl:choose>
<xsl:when test="contains($value, '"')">
<xsl:variable name="x" select="replace($value, '"', '""')"/>
<xsl:value-of select="concat('"', $x, '"')"/>
</xsl:when>
<xsl:when test="contains($value, $delimiter)">
<xsl:value-of select="concat('"', $value, '"')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$value"/>
</xsl:otherwise>
</xsl:choose>-->
<!-- Add the delimiter unless we are the last expression -->
<xsl:if test="position() != last()">
<xsl:value-of select="$delimiter"/>
</xsl:if>
</xsl:for-each>
<!-- Add a newline at the end of the record -->
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
"Internal error" means it's a problem in the XSLT processor, not in your code. You can consider things like checking known bugs and upgrading to the latest maintenance release; when you've done that, report it to the vendor.
Another technique is to try a different XSLT processor and see if it comes up with anything. When I run this on the current Saxon version (9.9.1.5), I get:
Static error at xsl:template on line 31 column 53 of test.xsl:
XTSE0120: No character data is allowed between top-level elements
Errors were reported during stylesheet compilation
So it could be that if you fix the problem in your stylesheet, the failure will go away. (The characters in question are not actually on line 31, they are the asterisks on line 46.)

Better way to cycle xsl:for-each letter of the alphabet?

I have a long XML file from which I ned to pull out book titles and other information, then sort it alphabetically, with a separator for each letter. I also need a section for items that don't begin with a letter, say a number or symbol. Something like:
#
1494 - hardcover, $9.99
A
After the Sands - paperback, $24.95
Arctic Spirit - hardcover, $65.00
B
Back to the Front - paperback, $18.95
…
I also need to create a separate list of authors, created from the same data but showing different kinds of information.
How I'm currently doing it
This is simplified, but I basically have this same code twice, once for titles and once for authors. The author version of the template works with different elements and does different things with the data, so I can't use the same template.
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'#'" />
</xsl:call-template>
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'A'" />
</xsl:call-template>
…
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'Z'" />
</xsl:call-template>
<xsl:template name="BIP-letter">
<xsl:param name="letter" />
<xsl:choose>
<xsl:when test="$letter = '#'">
<xsl:text>#</xsl:text>
<xsl:for-each select="//Book[
not(substring(Title,1,1) = 'A') and
not(substring(Title,1,1) = 'B') and
…
not(substring(Title/,1,1) = 'Z')
]">
<xsl:sort select="Title" />
<xsl:appy-templates select="Title" />
<!-- Add other relevant data here -->
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$letter" />
<xsl:for-each select="//Book[substring(Title,1,1) = $letter]">
<xsl:sort select="Title" />
<xsl:appy-templates select="Title" />
<!-- Add other relevant data here -->
</xsl:for-each>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
My questions
The code above works just fine, but:
Manually cycling through each letter gets very long, especially having to do it twice. Is there a way to simplify that? Something like a <xsl:for-each select="[A-Z]"> that I could use to set the parameter when calling the template?
Is there a simpler way to select all titles that don't begin with a letter? Something like //Book[not(substring(Title,1,1) = [A-Z])?
There may be cases where the title or author name starts with a lowercase letter. In the code above, they would get grouped with under the # heading, rather than with the actual letter. The only way I can think to accommodate that—doing it manually—would significantly bloat up the code.
This solution answers all questions asked:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="vLowercase" select="'abcdefghijklmnopqrstuvuxyz'"/>
<xsl:variable name="vUppercase" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:variable name="vDigits" select="'0123456789'"/>
<xsl:key name="kBookBy1stChar" match="Book"
use="translate(substring(Title, 1, 1),
'abcdefghijklmnopqrstuvuxyz0123456789',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ##########'
)"/>
<xsl:template match="/*">
<xsl:apply-templates mode="firstInGroup" select=
"Book[generate-id()
= generate-id(key('kBookBy1stChar',
translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)
)[1]
)
]">
<xsl:sort select="translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Book" mode="firstInGroup">
<xsl:value-of select="'
'"/>
<xsl:value-of select="translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)"/>
<xsl:apply-templates select=
"key('kBookBy1stChar',
translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)
)">
<xsl:sort select="Title"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Book">
<xsl:value-of select="'
'"/>
<xsl:value-of select="concat(Title, ' - ', Binding, ', $', price)"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the following xml document (none provided in the question!):
<Books>
<Book>
<Title>After the Sands</Title>
<Binding>paperback</Binding>
<price>24.95</price>
</Book>
<Book>
<Title>Cats Galore: A Compendium of Cultured Cats</Title>
<Binding>hardcover</Binding>
<price>5.00</price>
</Book>
<Book>
<Title>Arctic Spirit</Title>
<Binding>hardcover</Binding>
<price>65.00</price>
</Book>
<Book>
<Title>1494</Title>
<Binding>hardcover</Binding>
<price>9.99</price>
</Book>
<Book>
<Title>Back to the Front</Title>
<Binding>paperback</Binding>
<price>18.95</price>
</Book>
</Books>
the wanted, correct result is produced:
#
1494 - hardcover, $9.99
A
After the Sands - paperback, $24.95
Arctic Spirit - hardcover, $65.00
B
Back to the Front - paperback, $18.95
C
Cats Galore: A Compendium of Cultured Cats - hardcover, $5.00
Explanation:
Use of the Muenchian method for grouping
Use of the standard XPath translate() function
Using mode to process the first book in a group of books starting with the same (case-insensitive) character
Using <xsl:sort> to sort the books in alphabetical orser
The most problematic part is this:
I also need a section for items that don't begin with a letter, say a number or symbol.
If you have a list of all possible symbols that an item can begin with, then you can simply use translate() to convert them all to the # character. Otherwise it gets more complicated. I would try something like:
XSLT 1.0 (+ EXSLT node-set())
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:key name="book" match="Book" use="index" />
<xsl:template match="/Books">
<!-- first-pass: add index char -->
<xsl:variable name="books-rtf">
<xsl:for-each select="Book">
<xsl:copy>
<xsl:copy-of select="*"/>
<index>
<xsl:variable name="index" select="translate(substring(Title, 1, 1), 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" />
<xsl:choose>
<xsl:when test="contains('ABCDEFGHIJKLMNOPQRSTUVWXYZ', $index)">
<xsl:value-of select="$index"/>
</xsl:when>
<xsl:otherwise>#</xsl:otherwise>
</xsl:choose>
</index>
</xsl:copy>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="books" select="exsl:node-set($books-rtf)/Book" />
<!-- group by index char -->
<xsl:for-each select="$books[count(. | key('book', index)[1]) = 1]">
<xsl:sort select="index"/>
<xsl:value-of select="index"/>
<xsl:text>
</xsl:text>
<!-- list books -->
<xsl:for-each select="key('book', index)">
<xsl:sort select="Title"/>
<xsl:value-of select="Title"/>
<xsl:text> - </xsl:text>
<xsl:value-of select="Binding"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="Price"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
However, this still leaves the problem of items that begin with a diacritic, e.g. "Österreich" or say a Greek letter. Under this method they too will be clumped under #.
Unfortunately, the only good solution for this is to move to XSLT 2.0.
Demo: https://xsltfiddle.liberty-development.net/jyRYYjj/2

Displaying xml nodes dynamically and applying style to specific nodes using recursion

Below is my xml file.
<xml>
<top>
<main>
<firstname>John</firstname>
<lastname>John</lastname>
<table></table>
<chapter>
<firstname>Alex</firstname>
<lastname>Robert</lastname>
<p>Sample text chap</p>
<figure name="f1.svg"></figure>
<chapter>
<firstname>Rebec</firstname>
<lastname></lastname>
<p>Sample text</p>
<figure name="f2.svg"></figure>
</chapter>
</chapter>
</main>
</top>
</xml>
Desired output:
<bold>John
table
<bold>Robert
Sample text chap
f1.svg
<bold> Rebec
Sample text
f2.svg
Explaination: I have written an xslt to do this. I need to fetch the xml nodes dynamically. I cannot write: xsl:apply-templates select='main/lastname'. Because my xml format could change anytime.
I have tried a logic to first fetch all the xml nodes using '$root/*'. Then if 'table' element is encountered, i use xsl:apply-templates select='current()[name() = 'TABLE']' and perform table creation operations.
This works fine. I get the desired output but my figure elements only displays f1.svg at every place in the output. f2.svg is not shown.
And how do I match only 'lastname' and make it bold?
I want to make the code as generic/modular as possible so that it loops through all the elements of the xml tree and does some formatting on the specific nodes.
Below is a recursive xslt. With this my data is getting repeated. I am writing recursive template because xslt is not sequential.
XSLT:
<xsl:call-template name="FetchNodes">
<xsl:with-param name="endIndex" select="$NumberOfNodes" />
<xsl:with-param name="startIndex" select="1" />
<xsl:with-param name="context" select="$root/*" />
</xsl:call-template>
<xsl:template name="FetchNodes">
<xsl:param name="endIndex" />
<xsl:param name="startIndex" />
<xsl:param name="context" />
<xsl:if test="$startIndex <= $endIndex">
<xsl:if test="$context[$startIndex][name() = 'table']"">
<xsl:apply-templates select="$context[$startIndex][name() = 'table']"" mode="table" />
</xsl:if>
<xsl:call-template name="FetchNodes">
<xsl:with-param name="endIndex" select="$endIndex" />
<xsl:with-param name="startIndex" select="$startIndex + 1"/>
<xsl:with-param name="context" select="$context" />
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template match="node()" mode="table">
<xsl:value-of select="node()" />
</xsl:template>
With the above xslt, something is incorrect in the xpath of apply templates. Output is not proper.
I want XSL FO output.
Can anybody suggest something?
The problem it displaying "f1.svg" instead of "f2.svg" is because of this line
<xsl:variable name="ImageName">
<xsl:value-of select="$root/*/chapter/figure/#name" />
</xsl:variable>
You are already positioned on a figure at this point, so you only need to use a relative xpath expression here. The one you are currently using is an absolute path and so will always return the first #name attribute regardless of your context. It should look this this
<xsl:variable name="ImageName">
<xsl:value-of select="#name" />
</xsl:variable>
Or better still, like this
<xsl:variable name="ImageName" select="#name" />
Having said, the code is in a template that is trying to match an element a FIGURE element, which does not exist in the XML you have shown us. You can actually simplify the template match to this, for example
<xsl:template match="figure" mode="figure">
As for making things bold, you can just add the font-weight attribute to any block you want to make bold. Something like this:
<xsl:choose>
<xsl:when test="self::lastname">
<fo:inline font-weight="bold"><xsl:value-of select="text()" /></fo:inline>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="text()" />
</xsl:otherwise>
</xsl:choose>
EDIT: Having said all that, you may not be taking the correct approach to the problem. It may be better to use template matching, taking advantage of XSLT's built-in template to navigate over the document. Essentially, just write a template for each element you want to match, and generate the output, and then carry on matching its children.
For example, to turn a chapter into an fo:block do this
<xsl:template match="chapter">
<fo:block>
<xsl:apply-templates/>
</fo:block>
</xsl:template>
To output the firstname in bold, do this
<xsl:template match="firstname">
<fo:inline font-weight="bold">
<xsl:value-of select="text()"/>
</fo:inline>
</xsl:template>
To turn a figure into an image, do this (Note the use of Attribute Value Templates here, the curly braces indicate an expression to be evaluated, not output literally)
<xsl:template match="figure">
<fo:block>
<fo:external-graphic src="../resources/{#name}" content-height="60%" scaling="uniform" padding-left="2cm"/>
</fo:block>
</xsl:template>
Try this XSLT as a starting point, and build on it
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="main">
<fo:block>
<xsl:apply-templates/>
</fo:block>
</xsl:template>
<xsl:template match="chapter">
<fo:block>
<xsl:apply-templates/>
</fo:block>
</xsl:template>
<xsl:template match="firstname">
<fo:inline font-weight="bold">
<xsl:value-of select="text()"/>
</fo:inline>
</xsl:template>
<xsl:template match="lastname"/>
<xsl:template match="figure">
<fo:block>
<fo:external-graphic src="../resources/{#name}" content-height="60%" scaling="uniform" padding-left="2cm"/>
</fo:block>
</xsl:template>
</xsl:stylesheet>

Retrieving nodes having (or not) a child to apply a conditional template

I read lot of articles but did not find a conclusive help to my problem.
I have an XML document to which I apply an xslt to get a csv file as output.
I send a parameter to my xsl transformation to filter the target nodes to apply the templates.
The xml document looks like that (I removed some unuseful nodes for comprehension):
<GetMOTransactionsResponse xmlns="http://www.exane.com/pott" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.exane.com/pott PoTTMOTransaction.xsd">
<MOTransaction>
<Transaction VersionNumber="2" TradeDate="2013-11-20">
<TransactionId Type="Risque">32164597</TransactionId>
<InternalTransaction Type="Switch">
<BookCounterparty>
<Id Type="Risque">94</Id>
</BookCounterparty>
</InternalTransaction>
<SalesPerson>
<Id Type="Risque">-1</Id>
</SalesPerson>
</Transaction>
<GrossPrice>58.92</GrossPrice>
<MOAccount Account="TO1E" />
<Entity>0021</Entity>
</MOTransaction>
<MOTransaction>
<Transaction VersionNumber="1" TradeDate="2013-11-20">
<TransactionId Type="Risque">32164598</TransactionId>
<SalesPerson>
<Id Type="Risque">-1</Id>
</SalesPerson>
</Transaction>
<GrossPrice>58.92</GrossPrice>
<MOAccount Account="TO3E" />
<Entity>0021</Entity>
</MOTransaction>
</GetMOTransactionsResponse>
My xslt is below (sorry it's quite long, and I write it more simple than it really is):
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:pott="http://www.exane.com/pott">
<xsl:output method="text" omit-xml-declaration="no" indent="no" />
<xsl:param name="instrumentalSystem"></xsl:param>
<xsl:template name="abs">
<xsl:param name="n" />
<xsl:choose>
<xsl:when test="$n = 0">
<xsl:text>0</xsl:text>
</xsl:when>
<xsl:when test="$n > 0">
<xsl:value-of select="format-number($n, '#')" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="format-number(0 - $n, '#')" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="outputFormat">
<!--Declaration of variables-->
<xsl:variable name="GrossPrice" select="pott:GrossPrice" />
<xsl:variable name="TransactionId" select="pott:Transaction/pott:TransactionId[#Type='Risque']" />
<xsl:variable name="VersionNumber" select="pott:Transaction/#VersionNumber" />
<!--Set tags values-->
<xsl:value-of select="$Entity" />
<xsl:text>;</xsl:text>
<xsl:value-of select="concat('0000000', pott:MOAccount/#Account) "/>
<xsl:text>;</xsl:text>
<xsl:text>;</xsl:text>
<xsl:value-of select="$TransactionId" />
<xsl:text>;</xsl:text>
<xsl:value-of select="$VersionNumber" />
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template match="/">
<xsl:choose>
<!-- BB -->
<xsl:when test="$instrumentalSystem = 'BB'">
<!--xsl:for-each select="pott:GetMOTransactionsResponse/pott:MOTransaction/pott:Transaction[pott:InternalTransaction]"-->
<xsl:for-each select="pott:GetMOTransactionsResponse/pott:MOTransaction/pott:Transaction[pott:InternalTransaction]">
<xsl:call-template name="outputFormat"></xsl:call-template>
</xsl:for-each>
</xsl:when>
<!-- CP -->
<xsl:when test="$instrumentalSystem = 'CP'">
<xsl:for-each select="pott:GetMOTransactionsResponse/pott:MOTransaction/pott:Transaction[not(pott:InternalTransaction)]">
<xsl:call-template name="outputFormat"></xsl:call-template>
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
If parameter = BB, I want to select MOTransaction nodes that have a child Transaction that contains a InternalTransaction node.
If parameter = CP, I want to select MOTransaction nodes that don't have a child Transaction that contains a InternalTransaction node
When I write
pott:GetMOTransactionsResponse/pott:MOTransaction/pott:Transaction[pott:InternalTransaction], I get the Transaction nodes and not the MOTransaction nodes
I think I am not very far from the expected result, but despite all my attempts, I fail.
If anyone can help me.
I hope being clear, otherwise I can give more information.
Looking at one of xsl:for-each statements, you are doing this
<xsl:for-each select="pott:GetMOTransactionsResponse/pott:MOTransaction/pott:Transaction[pott:InternalTransaction]">
You say you want to select MOTransaction elements, but it is actually selecting the child Transaction elements. To match the logic you require, it should be this
<xsl:for-each select="pott:GetMOTransactionsResponse/pott:MOTransaction[pott:Transaction[pott:InternalTransaction]]">
In fact, this should also work
<xsl:for-each select="pott:GetMOTransactionsResponse/pott:MOTransaction[pott:Transaction/pott:InternalTransaction]">
Similarly, for the second statement (in the case of the parameter being "CP"), it could look like this
<xsl:for-each select="pott:GetMOTransactionsResponse/pott:MOTransaction[pott:Transaction[not(pott:InternalTransaction)]]">
Alternatively, it could look like this
<xsl:for-each select="pott:GetMOTransactionsResponse/pott:MOTransaction[not(pott:Transaction/pott:InternalTransaction)]">
They are not quite the same though, as the first will only include MOTransaction elements that have Transaction child elements, whereas the second will include MOTransaction that don't have any Transaction childs at all.
As a slight aside, you don't really need to use an xsl:for-each and xsl:call-template here. It might be better to use template matching.
Firstly, try changing the named template <xsl:template name="outputFormat"> to this
<xsl:template match="pott:MOTransaction">
Then, you can re-write you merge the xsl:for-each and xsl:call-template into a single xsl:apply-templates call.
<xsl:apply-template select="pott:GetMOTransactionsResponse/pott:MOTransaction[pott:Transaction/pott:InternalTransaction]" />

Generate a breadcrumb trail with xsl from a node structure

I have difficulties to write a template that generates a breadcrumb trial out of a node structure. It is not working correctly up to now, there is some flaw in my thinking how it should walk the item path.
Consider the following page structure:
<!-- ===== SITE PAGE STRUCTURE ===================================== -->
<index>
<item section="home" id="index"></item>
<item section="service" id="index">
<item id="content-management-systems">
<item id="p1-1"/>
<item id="p1-2"/>
<item id="p1-3"/>
</item>
<item id="online-stores"></item>
<item id="search-engines-and-ir"></item>
<item id="web-applications"></item>
</item>
<item section="solutions" id="index">
<item id="document-clustering"></item>
</item>
<item section="company" id="index">
<item section="company" id="about"></item>
<item section="company" id="philosophy" ></item>
...
</item>
...
</item>
This site index represents a site-structure of xml content pages in its hierarchy (consider it to be a menu). It contains of sections, that represent the site sections just as home, company, service, solutions, etc. These sections can contain sub-sections with pages, or just regular content pages. A content page (its xml contents such as title, text content, etc) is identified by the #id attribute in the item tree. The #id attribute mainly is used to fetch the content of the entire page that will be rendered to html.
The breadcrumb template uses the item node #id attribute to get the title of the page (which will be shown in the breadcrumb trail).
I try to implement the following template that walks the tree by checking the target section attribute #section and the target page attribute #id in the tree. I expect it to walk the axis down until the target item_target is found by comparing the ancestors #section attribute and the #id with $item_target of each node in that axis.
For example: Attribute *$item_section=service* and the page id *target item_target=p1-1* should now recursively "walk" to the section branch "service" (depth 1), check if the target page #id is found on this level. In this case it is not found, so it makes the next recurive call (via apply-templates) to the next item node level (in this case it would be content-management-systems, there the target item page p1-1 is found, so the trail process is finished:
The result should like this:
home >> service >> content management systems >> p1-1
But unfortunately it is not working correct, at least not in every case. Also maybe it can be solved more easily. I try to implement it as an recursive template that walks from the top (level 0) to the target page (item node) as a leaf.
<!-- walk item path to generate a breadcrumb trail -->
<xsl:template name="breadcrumb">
<a>
<xsl:attribute name="href">
<xsl:text>/</xsl:text>
<xsl:value-of select="$req-lg"/>
<xsl:text>/home/index</xsl:text>
</xsl:attribute>
<xsl:value-of select="'Home'"/>
</a>
<xsl:apply-templates select="$content/site/index" mode="Item-Path">
<xsl:with-param name="item_section" select="'service'"/>
<xsl:with-param name="item_target" select="'search-engines-and-ir'"/>
<xsl:with-param name="depth" select="0"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="item" mode="Item-Path">
<xsl:param name="item_section" />
<xsl:param name="item_target" />
<xsl:param name="depth" />
<!--
depth=<xsl:value-of select="$depth"/>
count=<xsl:value-of select="count(./node())"/><br/>
-->
<xsl:variable name="cur-id" select="#id"/>
<xsl:variable name="cur-section" select="#section"/>
<xsl:choose>
<xsl:when test="#id=$item_target">
>>
<a>
<xsl:attribute name="href">
<xsl:text>/</xsl:text>
<!-- req-lg: global langauge variable -->
<xsl:value-of select="$req-lg"/>
<xsl:text>/</xsl:text>
<xsl:value-of select="$item_section"/>
<xsl:text>/</xsl:text>
<xsl:if test="$depth = 2">
<xsl:value-of select="../#id"/>
<xsl:text>/</xsl:text>
</xsl:if>
<xsl:value-of select="#id"/>
</xsl:attribute>
<xsl:value-of
select="$content/page[#id=$cur-id]/title"/>
</a>
</xsl:when>
<xsl:otherwise>
<xsl:if test="ancestor-or-self::item/#section = $item_section and count(./node()) > 0">
>>:
<a>
<xsl:attribute name="href">
<xsl:text>/</xsl:text>
<!-- req-lg: global langauge variable -->
<xsl:value-of select="$req-lg"/>
<xsl:text>/</xsl:text>
<xsl:value-of select="$item_section"/>
<xsl:text>/</xsl:text>
<xsl:if test="$depth = 2">
<xsl:value-of select="../#id"/>
<xsl:text>/</xsl:text>
</xsl:if>
<xsl:value-of select="#id"/>
</xsl:attribute>
<xsl:value-of
select="$content/page[#id=$cur-id and #section=$item_section]/title"/>
</a>
</xsl:if>
</xsl:otherwise>
</xsl:choose>
<xsl:apply-templates select="item" mode="Item-Path">
<xsl:with-param name="item_section" select="$item_section"/>
<xsl:with-param name="item_target" select="$item_target"/>
<xsl:with-param name="depth" select="$depth + 1"/>
</xsl:apply-templates>
</xsl:template>
So as the hardcoded parameters in the template breadcrumb, target section = 'service' and target page = 'search-engines-and-ir', I expect an output like
home >> service >> search-engines-and-ir
But the output is
home >> service >> content-management-systems >> search-engines-and-ir
which is obviously not correct.
Can anybody give me a hint how to correct this issue? It would be even more elegant to avoid that depth checking, but up to now I cannot think of a other way, I am sure there is a more elegant solution.
I work with XSLT 1.0 (libxml via PHP5).
Hope my question is clear enough, if not, please ask :-) Thanks for the help in advance!
As simple as this:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:key name="kNodeById" match="item" use="#id"/>
<xsl:template match="/">
<xsl:text>home</xsl:text>
<xsl:call-template name="findPath">
<xsl:with-param name="pStart" select="'service'"/>
<xsl:with-param name="pEnd" select="'search-engines-and-ir'"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="findPath">
<xsl:param name="pStart"/>
<xsl:param name="pEnd"/>
<xsl:for-each select=
"key('kNodeById', $pEnd)
[ancestor::item[#section=$pStart]]
[1]
/ancestor-or-self::item
[not(descendant::item[#section=$pStart])]
">
<xsl:value-of select=
"concat('>>', #id[not(../#section)], #section)"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
the wanted, correct result is produced:
home>>service>>search-engines-and-ir
Do Note:
This solution prints the breadcrumb from any node -- anywhere in the hierarchy to any of its descendent nodes -- anywhere in the hierarchy. More precisely, for the first item (in document order) with id attribute equal to $pEnd, the breadcrumb is generated from its inner-most ancestor whose section attribute is equal to $pStart -- to that item element.
This solution should be much more efficient than any solution using //, because we are using a key to locate efficiently the "end" item element.
II. XSLT 2.0 solution:
Much shorter and easier -- an XPathe 2.0 single expression:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:key name="kNodeById" match="item" use="#id"/>
<xsl:template match="/">
<xsl:value-of select=
"string-join(
(
'home',
key('kNodeById', $pEnd)
[ancestor::item[#section=$pStart]]
[1]
/ancestor-or-self::item
[not(descendant::item[#section=$pStart])]
/(#id[not(../#section)], #section)[1]
),
'>>'
)
"/>
</xsl:template>
</xsl:stylesheet>
You can just iterate over the ancestor-or-self:: axis . This is easy to do without recursion. For example...
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html"/>
<xsl:template match="/">
<html><body>
<xsl:call-template name="bread-crumbs">
<xsl:with-param name="items" select="*/item" />
<xsl:with-param name="section" select="'service'" />
<xsl:with-param name="leaf" select="'p1-2'" />
</xsl:call-template>
</body></html>
</xsl:template>
<xsl:template name="bread-crumbs">
<xsl:param name="items" />
<xsl:param name="section" />
<xsl:param name="leaf" />
<xsl:value-of select="concat($section,'>>')" />
<xsl:for-each select="$items/self::item[#section=$section]//item[#id=$leaf]/
ancestor-or-self::item[not(#section)]">
<xsl:value-of select="#id" />
<xsl:if test="position() != last()">
<xsl:value-of select="'>>'" />
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
...on your sample input yields...
<html>
<body>service>>content-management-systems>>p1-2</body>
</html>
...which renders as...
service>>content-management-systems>>p1-2