xml creation with xslt (items per page with delta) - xslt

I'm using XSLT 1.0 to transform some XML documents. I have a problem to do one thing, in that I would need some help please:
I have a set of items in my XML document something like :
<item1>...</item1>
<item2>...</item2>
<!-- ... -->
<itemN>...</itemN>
I want to create something like this based on delta (items per page), for example delta is three:
<page>
<item1>...</item1>
<item2>...</item2>
<item3>...</item3>
</page>
<page>
<item4>...</item4>
<item5>...</item5>
<item6>...</item6>
</page>
So, basically to create a XML structure in XSLT that will put fixed number of items per page.
If tried something like this (in order to use node-set):
<xsl:variable name="all_items_per_delta">
<xsl:for-each select="item">
<!-- if position is one or if mod of 3 then insert a page node,
so for first contract and for 3th, 6th, 9th... item -->
<xsl:if test="position() = 1">
<page>
</xsl:if>
<!-- in the meantime for every contract insert the contract node -->
<item>
<!-- ... -->
</item>
<!-- if position is one or if mod of 3 then insert a page node,
so for first item and for 3th, 6th, 9th... item-->
<xsl:if test="( (position() mod 3) = 0) ) and ( position() != last() )">
</page>
<page>
</xsl:if>
<!-- if the position is last just end the page element -->
<xsl:if test="position() = last()"> `
</page>
</xsl:if>
</xsl:for-each>
</xsl:variable>
But this doesn't work because the xslt parser says
'Expected end of tag 'page'
and this is for the first <page> tag I want to add. It seems that I have to end the page tab </page> in the same line. With <xslt:if> it doesn't work unfortunately.
Please advise, how can I make this kind of tree fragment structure in order to later extract the node-set using EXSL extension.
Thank you.

The reason why it doesn't work is because XSLT must itself be well-formed XML; and e.g. this:
<xsl:if test="...">
</page>
</xsl:if>
isn't well-formed XML. Simply put, you can't have an opening tag inside one construct, and closing one inside another.
Now as to how to handle this properly. One simple way is to take every _n_th element in the outer loop, and generate <page> element for it; then put that element, and the following n-1 elements, inside that <page>:
<xsl:for-each select="item[position() mod $n = 0]">
<page>
<xsl:copy-of select=". | following-sibling::item[position() < $n)]">
</page>
</xsl:for-each>
Alternatively, you might want to use Muenchean method, grouping items by position() mod $n.

I used Pavel's answer, slightly altered, for pagination:
<xsl:param name="itemsPerPage" select="10"/>
<xsl:for-each select="item[position() mod $itemsPerPage = 1 or position() = 1]">
<page>
<xsl:for-each select=". | following-sibling::item[position() < $itemsPerPage]" >
<!-- code to print out items -->
</xsl:for-each>
</page>
</xsl:for-each>
I think you need the or position() = 1 to include the first items in the list. Also, note the mod $itemsPerPage = 1. For example, if you want to have 10 items per page, you want to select items 1, 11, 21, etc in the first for-each loop.

This requires an approach to grouping in XSLT. XSLT 1.0 provides little support and it needed the genius of the Munchean method to solve it. XSLT 2.0 has more support (but not all environments, e.g. some MS environments) support XSLT2.0.
Some resources (from helpfule XML experts) are:
http://www.jenitennison.com/xslt/grouping/
http://www.dpawson.co.uk/xsl/sect2/N4486.html
http://www.xml.com/pub/a/2003/11/05/tr.html

Related

Testing text node in XSLT

I have the following mix-content element:
<firstElement type="random">text1<secondElement>random_value</secondElement>text2</firstElement>
I want to make a for-each loop on <firstElement> child nodes with a nested if condition, for example:
<xsl:for-each select="child::*">
<xsl:if test="some test">
<xsl:copy>
</xsl:if>
</xsl:for-each>
How can I write a test that selects only the text nodes of <firstElement>?
I have tried with text(), but it only works for the child text node of the contextual node and self::text() doesn't seem to be a proper Xpath. I also tried to use XSLT 2.0 instance of to test against xs:string but it didn't work either.
How can I write a test that selects only the text nodes of
<firstElement>?
It is really difficult to understand your question:
First, a test does not select anything - I suppose you mean you want the test to pass text nodes only.
Next, when you do:
<xsl:for-each select="child::*">
you are selecting element nodes only - so any subsequent test that passes text nodes only will always return false.
It's also not clear why you need to select any nodes that are not text nodes in the first place, and then test them and pass only text nodes. But supposing that's really what you want to do, you can do it this way:
<xsl:template match="firstElement">
<output>
<!-- select all child nodes -->
<xsl:for-each select="node()">
<!-- pass only text nodes -->
<xsl:if test=". instance of text()">
<xsl:copy/>
</xsl:if>
</xsl:for-each>
</output>
</xsl:template>
This returns:
<output>text1text2</output>
which is the same result returned by the much simpler:
<xsl:template match="firstElement">
<output>
<xsl:copy-of select="text()"/>
</output>
</xsl:template>
I have tried with text(), but it only works for the child text node of the contextual node and self::text() doesn't seem to be a proper Xpath.
I am not sure why you think so.
<xsl:if test="self::text()">
works just as well - and has the advantage of being backward-compatible with XSLT 1.0.

check if repeating node is empty in xslt

I have xml like defined below . The node EducationDetails can repeat (unbounded).
<PersonalDetailsResponse>
<FirstName></FirstName>
<LastName></LastName>
<EducationDetails>
<Degree></Degree>
<Institution></Institution>
<Year></Year>
</EducationDetails>
<EducationDetails>
<Degree></Degree>
<Institution></Institution>
<Year></Year>
</EducationDetails>
</PersonalDetailsResponse>
I want to create another xml from the above one using xslt.
My requirement is, if there is no data in any of the EducationDetails child nodes , then the resulting xml has to get data from another source.
My problem is , I am not able to check if all the EducationDetails child nodes are empty.
Since variable value cannot be changed in xslt , I tried using saxon with below code.
xmlns:saxon="http://saxon.sf.net/" extension-element-prefixes="saxon"
<xsl:variable name="emptyNode" saxon:assignable="yes" select="0" />
<xsl:when test="count(ss:education) > 0">
<xsl:for-each select="ss:education">
<xsl:if test="not(*[.=''])">
<saxon:assign name="emptyNode">
<xsl:value-of select="1" />
</saxon:assign>
</xsl:if>
</xsl:for-each>
<xsl:if test="$emptyNode = 0">
<!-- Do logic if all educationdetails node is empty-->
</xsl:if>
</xsl:when>
But it throwing exception "net.sf.saxon.trans.XPathException: Unknown extension instruction " .
It looks like saxon 9 jar is required for it ,which I am not able to get from my repository.
Is there a simpler way to check if all the child nodes of are empty.
By empty I mean, child nodes might be present, but no value in them.
Well, if you use <xsl:template match="PersonalDetailsResponse[EducationDetails[*[normalize-space()]]">...</xsl:template> then you match only on PersonalDetailsResponse element having at least one EducationDetails element having at least one child element with non whitespace data. As you seem to use an XSLT 2.0 processor you can also use the perhaps clearer <xsl:template match="PersonalDetailsResponse[some $ed in EducationDetails/* satisfies normalize-space($ed)]">...</xsl:template>.
Or perhaps if you want a variable inside the template use
<xsl:template match="PersonalDetailsResponse">
<xsl:variable name="empty-details" select="not(EducationDetails/*[normalize-space()])"/>
<xsl:if test="$empty-details">...</xsl:if>
</xsl:template>
With XSLT 2.0 the use of some or every satisfies might be easier to understand e.g.
<xsl:template match="PersonalDetailsResponse">
<xsl:variable name="empty-details" select="every $dt in EducationDetails/* satisfies not(normalize-space($dt))"/>
<xsl:if test="$empty-details">...</xsl:if>
</xsl:template>
But usually writing templates with appropriate match conditions saves you from using xsl:if or xsl:choose inside of a template.

XSLT position() - exlude elements that fail test condition

lets say I have the following xml file:
<jobs>
<job>
<PositionTitle>Painter</PositionTitle>
<InternalOrExternal>External</InternalOrExternal>
</job>
<job>
<PositionTitle>Plumber</PositionTitle>
<InternalOrExternal>Internal</InternalOrExternal>
</job>
<job>
<PositionTitle>Chemist</PositionTitle>
<InternalOrExternal>Internal</InternalOrExternal>
</job>
<job>
<PositionTitle>Teacher</PositionTitle>
<InternalOrExternal>External</InternalOrExternal>
</job>
</jobs>
.
I process it with the following xslt to show only the external jobs:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:content="http://purl.org/rss/1.0/modules/content/" >
<xsl:output method="html"/>
<xsl:template match="/">
<xsl:for-each select="jobs/job">
<xsl:if test="InternalOrExternal = 'External'"><!-- Only show external jobs -->
<xsl:value-of select="PositionTitle"/> - <xsl:value-of select="position()"/><br />
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
This results as:
Painter - 1
Teacher - 4
I assume the position() function returns the actual position in the xml file and doesn't take into account any if statements that may exclude some jobs. In this case the Internal jobs are excluded but their position is still counted.
The result that I want is:
Painter - 1
Teacher - 2
Is their any way I can get the position() function to only count what I display?
I've tried this with no luck:
<xsl:value-of select="position(jobs/job[InternalOrExternal='External'])"/>
position() gives you the position of the current node within the "current node list", i.e. whatever list of nodes were selected by the most recent for-each or apply-templates. You can think of this informally as the current iteration number of the for-each1. By saying
<xsl:for-each select="jobs/job">
the current node list is all the job elements, so you get position 1 for the first one and position 4 for the fourth one. If you remove the xsl:if and instead move the filtering into a predicate on the for-each selector:
<xsl:for-each select="jobs/job[InternalOrExternal='External']">
<xsl:value-of select="PositionTitle"/> - <xsl:value-of select="position()"/><br />
</xsl:for-each>
then the current node list only includes the "External" job elements, and you'll get the position() values you require.
1 Since XSLT instructions don't have side effects (e.g. there are no updateable variables), the XSLT processor doesn't necessarily have to implement for-each internally using a sequential loop. It could choose to process different nodes in parallel or out of order, as long as it assembles the output in such a way that it looks the same as a sequential loop would produce.
In addition to Ian Roberts' great answer: If you had to have the context be job (for whatever reason), you could also use xsl:number instead of position().
Example:
<xsl:value-of select="PositionTitle"/> - <xsl:number count="job[InternalOrExternal='External']"/><br />

Nested for-each loops, accessing outer element with variable from the inner loop

I'm trying to write an XSL that will output a certain subset of fields from the source XML. This subset will be determined at transformation time, by using an external XML configuration document containing the field names, and other specific information (such as the padding length).
So, this is two for-each loops:
The outer one iterates over the records to access their fields record by record.
The inner one iterates over the configuration XML document to grab the configured fields from the current record.
I've seen in In XSLT how do I access elements from the outer loop from within nested loops? that the current element in the outside loop can be stored in an xsl:variable. But then I've got to define a new variable inside the inner loop to get the field name. Which yields to the question: Is it possible to access a path in which there are two variables ?
For instance, the source XML document looks like:
<data>
<dataset>
<record>
<field1>value1</field1>
...
<fieldN>valueN</fieldN>
</record>
</dataset>
<dataset>
<record>
<field1>value1</field1>
...
<fieldN>valueN</fieldN>
</record>
</dataset>
</data>
I'd like to have an external XML file looking like:
<configuration>
<outputField order="1">
<fieldName>field1</fieldName>
<fieldPadding>25</fieldPadding>
</outputField>
...
<outputField order="N">
<fieldName>fieldN</fieldName>
<fieldPadding>10</fieldPadding>
</outputField>
</configuration>
The XSL I've got so far:
<xsl:variable name="config" select="document('./configuration.xml')"/>
<xsl:for-each select="data/dataset/record">
<!-- Store the current record in a variable -->
<xsl:variable name="rec" select="."/>
<xsl:for-each select="$config/configuration/outputField">
<xsl:variable name="field" select="fieldName"/>
<xsl:variable name="padding" select="fieldPadding"/>
<!-- Here's trouble -->
<xsl:variable name="value" select="$rec/$field"/>
<xsl:call-template name="append-pad">
<xsl:with-param name="padChar" select="$padChar"/>
<xsl:with-param name="padVar" select="$value"/>
<xsl:with-param name="length" select="$padding"/>
</xsl:call-template>
</xsl:for-each>
<xsl:value-of select="$newline"/>
</xsl:for-each>
I'm quite new to XSL, so this might well be a ridiculous question, and the approach can also be plain wrong (i.e. repeatig inner loop for a task that could be done once at the beggining). I'd appreciate any tips on how to select the field value from the outer loop element and, of course, open to better ways to approach this task.
Your stylesheet looks almost fine. Just the expression $rec/$field doesn't make sense because you can't combine two node sets/sequences this way. Instead, you should compare the names of the elements using the name() function. If I understood your problem correctly, something like this should work:
<xsl:variable name="config" select="document('./configuration.xml')"/>
<xsl:for-each select="data/dataset/record">
<xsl:variable name="rec" select="."/>
<xsl:for-each select="$config/configuration/outputField">
<xsl:variable name="field" select="fieldName"/>
...
<xsl:variable name="value" select="$rec/*[name(.)=$field]"/>
...
</xsl:for-each>
<xsl:value-of select="$newline"/>
</xsl:for-each>
Variable field is not required in this example. You can also use function current() to access the current context node of the inner loop:
<xsl:variable name="value" select="$rec/*[name(.)=current()/fieldName]"/>

Need XSLT for multi-line item

I already tried a lot of things here on stack overflow but I still face the same problem.
Let me try to explain my issue and what I need to achieve. For this I have the following XML:
<authorizationGroups>
<authorizationGroup> <!-- can be multiple -->
<name>OGroup 1</name>
<application> <!-- can be multiple -->
<uid>646</uid>
<applicationFunctions> <!-- can be multiple -->
<name>auth function 11</name>
<name>auth function 12</name>
</applicationFunctions>
</application>
<role>5000682864</role>
<role>5000685391</role>
</authorizationGroup>
<authorizationGroup> <!-- can be multiple -->
<name>OGroup 8</name>
<application> <!-- can be multiple -->
<uid>646</uid>
<applicationFunctions> <!-- can be multiple -->
<name>auth function 13</name>
<name>auth function 14</name>
</applicationFunctions>
</application>
<role>5000683374</role>
<role>5000685391</role>
</authorizationGroup>
I need to get out something like this:
<resource-types>
<resource-types>
<resource-type>
<name>OGroup 1</name>
<actions>
auth function 11,
auth function 12
</actions>
</resource-type>
<resource-type>
<name>OGroup 8</name>
<actions>
auth function 13,
auth function 14
</actions>
</resource-type>
</resource-types>
My problem is that when I use the XSLT I always end up with all the "auth functions xx" within one .
My current code snippet looks like this (there is obviously more than this part):
<resource-types>
<xsl:call-template name="resource_types"/>
</resource-types>
<xsl:template name="resource_types">
<resource-types>
<xsl:for-each select="/authorizationGroups/authorizationGroup/name">
<resource-type>
<name>
<xsl:value-of select="text()"/>
</name>
<actions>
<xsl:for-each select="/authorizationGroups/authorizationGroup/application/applicationFunctions">
<xsl:value-of select=".//text()"/>
,
</xsl:for-each>
</actions>
</resource-type>
</xsl:for-each>
</resource-types>
</xsl:template>
Now I receive all "auth functions xx" in one go. My understanding of XSLT is limited, so my main question is how can I limit the search for the to the part of the XML document I am in.
I assumed that this was a very easy action, but after three days research on the net and stack overflow I haven't come up with an answer.
cu
Andreas
The issue is, I think, is that you are iterating over name elements, like so...
<xsl:for-each select="/authorizationGroups/authorizationGroup/name">
However, when you come to get the applicationFunctions within this xsl:for-each, you do this...
<xsl:for-each
select="/authorizationGroups/authorizationGroup/application/applicationFunctions">
Because of the forward slash at the start of the expression, this will get all applicationFunctions relative to the root of the document, and not the authorizationGroup you are currently in.
What you need to do is this...
<xsl:for-each select="../application/applicationFunctions">
The .. is to get the parent element, because you are currently positioned on the name element which is at the same level as the application element.
Actually, what would be slighty better would be it initially itereate over authorizationGroup to start with
<xsl:for-each select="/authorizationGroups/authorizationGroup">
And then iterate over the applicationFunctions like so
<xsl:for-each select="application/applicationFunctions">
Either enclose the line break in <xsl:text> like (untested):
<xsl:for-each select="/authorizationGroups/authorizationGroup/application/applicationFunctions">
<xsl:value-of select=".//text()"/>
<xsl:text>,
</xsl:text>
</xsl:for-each>
or include it as an escape in <xsl:value-of> like (untested):
<xsl:value-of select="concat(.//text(), ',
')"/>