Two questions about XSLT - xslt

I have now two questions about xslt.
First, I want to describe all of elements which without any attribute. Like
<element id="p0" attribute="a1"/>
<element id="p1"/>
<element id="p2"/>
I need a group of elements without attribute="a1", so just need p1 and p2. With XSLT, should I write <xsl:if test="element[#attribute]=''">? Because when I test it, I found it doesn't work. So please help me.
The second is, I want to make the output result not in the same line. Like
right:
t11
t22
t33
wrong:
t11t22t33
which XSLT word should I write? Thanks a bunch.

All relative paths in an xsl:template are evaluated against the node that was matched by the template. So <xsl:if test="element[#attribute]=''"> is testing for the presence of a child of the current node called element that matches your condition. If this test is located in a template that matches element nodes, then it's not going to work. You should use . to refer to the current node.
The boolean expression element[#attribute]='' is looking for an element node that has an attribute called attribute and that is itself empty. It doesn't test anything about the content of the attribute attribute.
Basically, you need to understand the following template:
<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:template match="element">
<xsl:text>element position=[</xsl:text>
<xsl:value-of select="count(preceding-sibling::element) + 1"/>
<xsl:text>] / </xsl:text>
<xsl:choose>
<xsl:when test=".[not(#attribute)]">
<xsl:text>no #attribute</xsl:text>
</xsl:when>
<xsl:when test=".[#attribute='']">
<xsl:text>empty #attribute</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>non-empty #attribute</xsl:text>
</xsl:otherwise>
</xsl:choose>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
Applied to this test document:
<root>
<element id="p0" attribute="a1">one</element>
<element id="p1" attribute="test"></element>
<element id="p1" attribute=""></element>
<element id="p2">three</element>
</root>
element position=[1] / non-empty #attribute
element position=[2] / non-empty #attribute
element position=[3] / empty #attribute
element position=[4] / no #attribute
This template also answers your second question about newlines.

With this stylesheet:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output indent="no" omit-xml-declaration="yes"/>
<xsl:template match="/">
<xsl:for-each select="root/element[not(#attribute='a1')]">
<xsl:value-of select="#id"/><xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
when applied to this input:
<root>
<element id="p0" attribute="a1"/>
<element id="p1"/>
<element id="p2"/>
</root>
outputs:
p1
p2

Related

How to not touch a record if text exist in an XML field but process the record in no text exist using XSLT 2

Using XSLT 2 how can I skip and not touch a record if a field contains text, in this case a date? I want to only process all the record that don't have a <SurveyDate> and don't touch record that already have a <SurveyDate>.
I tried using a choose statement with a test of "not(SurveyDate/text())" but this is not working. here is my complete XSL code:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0"
xmlns:lookup="lookup" xmlns:exsl="http://exslt.org/common" exclude-result-prefixes="lookup exsl">
<xsl:output method="xml" omit-xml-declaration="yes" indent="yes" encoding="utf-8" media-type="xml/plain" />
<xsl:strip-space elements="*" />
<xsl:template match="node() | #*">
<xsl:copy>
<xsl:apply-templates select="node() | #*" />
</xsl:copy>
</xsl:template>
<xsl:template match="Sub">
<!-- This is the final output -->
<xsl:choose>
<xsl:when test="not(SurveyDate/text())">
<xsl:if test= "count(Request/Phase/Status) = count(Request/Phase/Status[matches(. , 'Sup|Ser|Adm|Can')])">
<Request>
<xsl:copy-of select="Request/Code"/>
<SurveyDate>
<xsl:value-of select="format-dateTime(current-dateTime(), '[Y0001]-[M01]-[D01]T[H1]:[m01]:[s01]')"/>
</SurveyDate>
</Request>
</xsl:if>
</xsl:when>
<xsl:otherwise>
<!-- just for testing remove when done -->
<Test>Do nothing</Test>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
And this is my test XML data.
<?xml version='1.0' encoding='UTF-8'?>
<document>
<businessobjects>
<Sub>
<Code>1.02</Code>
<Status>UsrWorkOrderCancelled</Status>
<Request>
<Code>1.00</Code>
<Description>Test 1</Description>
<SurveyDate>2022-11-02T22:55:55</SurveyDate>
<Phase>
<Code>1.01</Code>
<Status>UsrWorkOrderSupervisorApproved</Status>
</Phase>
<Phase>
<Code>1.02</Code>
<Status>UsrWorkOrderCancelled</Status>
</Phase>
</Request>
</Sub>
<Sub>
<Code>2.01</Code>
<Status>UsrWorkOrderSupervisorApproved</Status>
<Request>
<Code>2.00</Code>
<Description>Test 2</Description>
<SurveyDate></SurveyDate>
<Phase>
<Code>2.01</Code>
<Status>UsrWorkOrderSupervisorApproved</Status>
</Phase>
<Phase>
<Code>2.02</Code>
<Status>UsrWorkOrderCancelled</Status>
</Phase>
</Request>
</Sub>
</businessobjects>
</document>
The result XML I need is this:
<document>
<businessobjects>
<Request>
<Code>2.00</Code>
<SurveyDate>2022-11-03T21:45:13</SurveyDate>
</Request>
</businessobjects>
</document>
My advice: forget using xsl:choose or xsl:if, and instead put the conditional logic into the template's match expression:
<xsl:template match="Sub[not(Request/SurveyDate/text())]">
<!-- handle Sub without SurveyDate -->
<!-- ... -->
</xsl:template>
Leave the case where a Sub does have a SurveyDate for the identity template to handle, if you want to copy it unchanged. If you want to remove it (it's not clear from your test code what you want to do with it), you could add another template to do so:
<xsl:template match="Sub"/>
Note that template would have a lower priority than the one above, because its match expression is simpler, so it would apply only to Sub elements which did have a SurveyDate descendant.

How do I iterate over an xs:list?

Consider the schema:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="TheList">
<xs:simpleType>
<xs:list itemType="xs:string" />
</xs:simpleType>
</xs:element>
</xs:schema>
And the xml:
<TheList>
This list has 5 values.
</TheList>
How can I iterate over each of the words in the list? To create something like:
<item>This</item>
<item>list</item>
<item>has</item>
<item>5</item>
<item>values.</item>
Based on the answers I've found here and here, I should do something like:
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="TheList">
<xsl:for-each select="tokenize(., ' ')">
<item><xsl:value-of select="." /></item>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
However, at least in Altova's XML spy, I am getting this error:
Wrong occurrence to match required sequence type: The supplied sequence ('5' item(s)) has the wrong occurrence to match the sequence type xs:string ('zero or one')
Using the built in debugger, I have been able to determine that the error is thrown when calling tokenize on an element that has been declared as an xs:list. Which makes sense, since the element should already be split according to the rules regarding xs:list. To me, this suggests:
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="TheList">
<xsl:for-each select=".">
<item><xsl:value-of select="." /></item>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
However, this treats the list as a single item and does not create a new item element for each word.
The for-each command seems to treat the xs:list as a single element while the tokenize function seems to treat the same xs:list as multiple elements. What am I missing?
If you're using a schema-aware transformation, then you don't need to tokenize the value yourself - the process of atomization does it for you automatically.
<xsl:template match="TheList">
<xsl:for-each select="data(.)">
<item><xsl:value-of select="." /></item>
</xsl:for-each>
</xsl:template>
If you want the code to work in both schema-aware and non-schema-aware environments you can write
<xsl:template match="TheList">
<xsl:for-each select="tokenize(string(.), ' ')">
<item><xsl:value-of select="." /></item>
</xsl:for-each>
</xsl:template>

XSLT correct approach in writing XPath expression used in multiple condition checking and displaying value

Considering this XML,
XML:
<?xml version="1.0" encoding="UTF-8"?>
<items>
<book>
<title>doublebell</title>
<count>available</count>
</book>
<phone>
<brand>nokia</brand>
<model></model>
</phone>
</items>
Mapping Criteria while writing XSLT:
show the newbook/newtitle only if a value is present in input.
show the newbook/newcount only if a value is present in input.
show the newphone/newbrand only if a value is present in input.
show the newphone/newmodel only if a value is present in input.
XSLT:
<?xml version="1.0" encoding="ISO-8859-1"?>
<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:variable name="book" select="items/book" />
<xsl:variable name="phone" select="items/phone" />
<xsl:template match="/">
<items>
<newbook>
<xsl:if test="$book/title!=''">
<newtitle>
<xsl:value-of select="$book/title" />
</newtitle>
</xsl:if>
<xsl:if test="$book/count!=''">
<newcount>
<xsl:value-of select="$book/count" />
</newcount>
</xsl:if>
</newbook>
<xsl:if test="$phone/brand!='' or $phone/model!=''"> <!-- not sure if this condition is required for the above mapping criteria -->
<newphone>
<xsl:if test="$phone/brand!=''">
<newbrand>
<xsl:value-of select="$phone/brand" />
</newbrand>
</xsl:if>
<xsl:if test="$phone/model!=''">
<newmodel>
<xsl:value-of select="$phone/model" />
</newmodel>
</xsl:if>
</newphone>
</xsl:if>
</items>
</xsl:template>
</xsl:stylesheet>
This is my concern:- In my actual XSLT, I have almost 70 conditions like
this, and everytime the XPath search is made twice [or thrice.. ] for
each condition [ for eg: <xsl:if test="$phone/brand!=''"> and <xsl:value-of select="$phone/brand" /> and outer if condition].
Is this much performance overhead? I don't feel it when I ran my application.
I like to hear from experienced people if this is correct way of writing the XSLT. Do I need to save the path in a variable and reuse it as done for $book
and $phone ? In such a case there will be 70+variables just to hold this.
You can approach this quite differently using templates. If you define a template that matches any element whose content is empty and does nothing:
<xsl:template match="*[. = '']" />
or possibly use normalize-space() if you want to consider elements to be empty if they contain only whitespace
<xsl:template match="*[not(normalize-space())]" />
Now with this in place add templates for the elements you are interested in
<xsl:template match="book">
<newbook><xsl:apply-templates /></newbook>
</xsl:template>
<xsl:template match="title">
<newtitle><xsl:apply-templates /></newtitle>
</xsl:template>
and so on. Now the book template will create a newbook element and go on to process its children. When it gets to the title it will have two different templates to choose from and will pick the "most specific" match. If the title is empty then the *[. = ''] template will win and nothing will be output, only if the title is non-empty will it create a newtitle element.
This way you let the template matcher do most of the work for you, you don't need any explicit conditional checks using xsl:if.
<?xml version="1.0" encoding="ISO-8859-1"?>
<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:template match="/">
<items><xsl:apply-templates select="items/*" /></items>
</xsl:template>
<!-- ignore empty elements -->
<xsl:template match="*[not(normalize-space())]" />
<xsl:template match="book">
<newbook><xsl:apply-templates /></newbook>
</xsl:template>
<xsl:template match="title">
<newtitle><xsl:apply-templates /></newtitle>
</xsl:template>
<!-- and so on with similar templates for the other elements -->
</xsl:stylesheet>
Building on Ian's answer, you can also make a generic template that will create the "new" elements for you without having to specify each one individually. That would look like the below:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" />
<xsl:template match="/">
<items><xsl:apply-templates select="items/*" /></items>
</xsl:template>
<xsl:template match="*[not(normalize-space())]" />
<xsl:template match="*">
<xsl:element name="{concat('new',name())}">
<xsl:apply-templates/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
That last template just rebuilds the element by concatenating the word "new" to the front of it.

Using XSLT 2.0 to parse the values of multiple attributes into an array-like structure

I'd like to be able to select all the attributes of a certain type in a document (for example, //#arch) and then take that node set and parse the values out into second node set. When I say "parse", in specific I mean I want to turn a node set like this:
arch="value1;value2;value3"
arch="value1:value4"
into a node set like this:
arch="value1"
arch="value2"
arch="value3"
arch="value1"
arch="value4"
or something like that; I want to get the individual values out of the attributes and into their own node.
If I can get it to that state, I've got plenty of methods for sorting and duplicate removal, after which I'd be using the finished node set for a publishing task.
I'm not so much looking for an tidy answer here as an approach. I know that XSLT cannot do dynamic arrays, but that's not the same as not being able to do something like dynamic arrays or something that mimics the important part of the functionality.
One thought that has occurred to me is that I could count the nodes in the first node set, and the number of delimiters, calculate the number of entries that the second node set would need and create it (somehow), and use the substring functions to parse out the first node set into the second node set.
There's usually a way working around XSLT's issues; has anyone worked their way around this one before?
Thanks for any help,
Jeff.
I think what you're looking for is a sequence. A sequence can be either nodes or atomic values (see http://www.w3.org/TR/xslt20/#constructing-sequences).
Here's an example showing the construction of a sequence and then iterating over it. The sequence is the atomic values from #arch, but it could also be nodes.
XML Input
<doc>
<foo arch="value1;value2;value3"/>
<foo arch="value1:value4"/>
</doc>
XSLT 2.0
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="archSequence" as="item()*">
<xsl:for-each select="//#arch">
<xsl:for-each select="tokenize(.,'[;:]')">
<xsl:value-of select="."/>
</xsl:for-each>
</xsl:for-each>
</xsl:variable>
<xsl:template match="/*">
<sequence>
<xsl:for-each select="$archSequence">
<item><xsl:value-of select="."/></item>
</xsl:for-each>
</sequence>
</xsl:template>
</xsl:stylesheet>
XML Output
<sequence>
<item>value1</item>
<item>value2</item>
<item>value3</item>
<item>value1</item>
<item>value4</item>
</sequence>
Example of a sequence of elements (same output):
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="archSequence" as="element()*">
<xsl:for-each select="//#arch">
<xsl:for-each select="tokenize(.,'[;:]')">
<item><xsl:value-of select="."/></item>
</xsl:for-each>
</xsl:for-each>
</xsl:variable>
<xsl:template match="/*">
<sequence>
<xsl:for-each select="$archSequence">
<xsl:copy-of select="."/>
</xsl:for-each>
</sequence>
</xsl:template>
</xsl:stylesheet>
You can use the tokenize function in a for expression to get a sequence of the separate values, then create an attribute node for each one. However, since XSLT doesn't let you create a bare attribute node with no element parent, you'll have to use a trick like this:
<xsl:variable name="archElements">
<xsl:for-each select="for $attr in $initialNodeSet
return tokenize($attr, '[:;]')">
<dummy arch="{.}" />
</xsl:for-each>
</xsl:variable>
and then $archElements/dummy/#arch should be the set of separated arch attribute nodes that you require.
Complete example:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output indent="yes" />
<xsl:template match="/">
<xsl:variable name="inputData">
<a arch="value1;value2;value3" />
<a arch="value1:value4" />
</xsl:variable>
<!-- create an example node set containing the two arch attribute nodes -->
<xsl:variable name="initialNodeSet" select="$inputData/a/#arch" />
<!-- tokenize and generate one arch attribute node for each value -->
<xsl:variable name="archElements">
<xsl:for-each select="for $attr in $initialNodeSet
return tokenize($attr, '[:;]')">
<dummy arch="{.}" />
</xsl:for-each>
</xsl:variable>
<!-- output to verify -->
<r>
<xsl:for-each select="$archElements/dummy/#arch">
<c><xsl:copy-of select="."/></c>
</xsl:for-each>
</r>
</xsl:template>
</xsl:stylesheet>
When run over any input document (the content is ignored) this produces
<?xml version="1.0" encoding="UTF-8"?>
<r>
<c arch="value1"/>
<c arch="value2"/>
<c arch="value3"/>
<c arch="value1"/>
<c arch="value4"/>
</r>

XSL condition to check if node exists

I want to check if in my XML exists node that has type attribute containing string type_attachment_.
Is it a correct way to check it?
<xsl:if test="count(*[contains(#Type, 'type_attachment_')]) > 0">
something
</xsl:if>
I don't know how nested can this node be. It can be for example as simple as that:
<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl"?>
<hello-world>
<greeter>
<dsdsds>An XSLT Programmer
<greeting type = 'type_attachment_'>Hello, World!
</greeting>
</dsdsds>
</greeter>
</hello-world>
but can also contain this node nested in different other elements.
Expressions that match existing nodes are truthy. Expressions that do not match any nodes are falsy.
Therefore, you don't need to count the set of nodes returned. Simply test to see if anything matches.
<xsl:if test="*[contains(#Type, 'type_attachment')]">
something
</xsl:if>
Find out an example:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:param name="filt">
<filters>
<ritem type="type_attachment_" relateditemnumber="8901037"/>
<ritem relateditemnumber="8901038"/>
<ritem type="type_attachment_" relateditemnumber="8901039"/>
<ritem relateditemnumber="8901040"/>
</filters>
</xsl:param>
<xsl:template match="/">
<xsl:for-each select="$filt/filters/ritem[#type='type_attachment_']">
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
OUTPUT:
<ritem type="type_attachment_" relateditemnumber="8901037"/>
<ritem type="type_attachment_" relateditemnumber="8901039"/>