Traverse xml document using xpath and xsl - xslt

I am using XSL and XPATH to traverse, read and transform a complex XML file where I have multiple nodes with same names but different values. My sample xml file is below -
<core>
<address>
<postalZipCode>90017</postalZipCode>
<updateSource>xxxxxx</updateSource>
<city>LOS ANGELES</city>
<stateProvince>CA</stateProvince>
<type>MAILING</type>
<line1>818 WEST SEVENTH STREET</line1>
</address>
<address>
<postalZipCode>95014</postalZipCode>
<updateSource>xxxxxx</updateSource>
<city>CUPERTINO</city>
<stateProvince>CA</stateProvince>
<type>PRIMARY</type>
<line1>1234 XYZ STREET</line1>
</address>
<memberId>0</memberId>
</core>
When I read the MAILING ADDRESS Line 1 value , the value I am getting is "1234 XYZ STREET" which essentially is the PRIMARY ADDRESS . Here is my xsl file snippet :-
<xsl:template match="core">
<xsl:if test="memberId['0'] and address/type['MAILING']">
<fo:table-row>
<fo:table-cell><fo:block /></fo:table-cell>
<fo:table-cell xsl:use-attribute-sets="data">
<fo:block>Line 1</fo:block>
</fo:table-cell>
<fo:table-cell xsl:use-attribute-sets="data">
<fo:block><xsl:value-of select="address/line1/text()"/>
</fo:block>
</fo:table-cell>
</fo:table-row>
Could the experts here please suggest if there is any other way possible to force XSL to read the correct value of where address is MAILING, instead of reading the first value it finds ???

if there is any other way possible to force XSL to read the correct
value of where address is MAILING, instead of reading the first value
it finds ???
Your question is rather confusing, because MAILING is the first address in your example - and the snippet you show does not produce the result you say it does.
In any case, to make sure you get data from the MAILING address regardless of its position, change this:
<xsl:value-of select="address/line1/text()"/>
to:
<xsl:value-of select="address[type='MAILING']/line1"/>
To understand how this works, read more about predicates.

The code you have written says "if there is an address with type MAILING, then print the value of address/line1 (whether or not this is in any way related to the address with type MAILING)".
In XSLT 1.0, when xsl:value-of selects more than one node, it displays the value of the first one it finds. In XSLT 2.0 it concatenates the values of all the selected nodes, with a separator that defaults to a single space.
If there's more than one node and you want to select a specific one, then select it with a predicate, e.g. xsl:value-of select="address[#type='MAILING']/line1.

Related

How can you apply concat(...) in a value-of directive in case of multiple nodes?

I am outputting the name node of each property node in a ; delimited string as following:
<xsl:value-of select="properties/property/name" separator=";" />
I want to alter this such that each element is prefixed with _. An example output should be:
_alpha;_beta;_gamma
I tried the following:
<xsl:value-of select="concat('_', properties/property/name)" separator=";" />
I want to use this to create an output node containing that string:
<my_node>
<xsl:value-of select="concat('_', properties/property/name)" separator=";" />
</my_node>
This gives an error when there are multiple properties:
XPTY0004: A sequence of more than one item is not allowed
as the second argument of fn:concat() (<name>, <name>)
Is there a way to get this working in XSLT 2.0/3.0?
I could resort to the XSLT 1.0 for-each solution as given in https://stackoverflow.com/a/57856287/12042211 (in which we are manually adding the separator), but I am wondering if something elegant in XSLT 2.0/3.0 is possible.
The answer is yes. XSLT 2.0 allows you to write expressions like this...
<xsl:value-of select="properties/property/concat('_', name)" separator=";" />
So, for each property it selects the concatenation of "_" with the name element.
Such syntax is not valid in XSLT 1.0 though.
In XSLT 3.0 I would tend to write this as
<xsl:value-of select="properties/property ! ('_' || name)" separator=";" />
and perhaps use string-join() instead of xsl:value-of. You haven't shown the context, but try to use xsl:value-of only when you really want a text node, not when you just want a string.

Applying Conditional Logic using XSL

I have an XML document where I get several addresses for a member as address type PRIMARY, MAILING etc. however I only want to read the address as PRIMARY, MAILING when memberId is '0'. Please see the sample xml below.
<core>
<address>
<postalZipCode>90017</postalZipCode>
<updateSource>xxxxxx</updateSource>
<city>LOS ANGELES</city>
<stateProvince>CA</stateProvince>
<type>MAILING</type>
<line1>818 WEST SEVENTH STREET</line1>
</address>
<address>
<postalZipCode>95014</postalZipCode>
<updateSource>xxxxxx</updateSource>
<city>CUPERTINO</city>
<stateProvince>CA</stateProvince>
<type>PRIMARY</type>
<line1>1234 XYZ STREET</line1>
</address>
<memberId>0</memberId>
</core>
The XSL condition I am trying to evaluate in my XSLT file is as below -
<xsl:template match="core">
<xsl:when test="memberId[.='0'] and address/type[.='PRIMARY']">
<fo:table-row>
<fo:table-cell xsl:use-attribute-sets="data">
<fo:block>Line 1</fo:block>
</fo:table-cell>
</fo:table-row>
But this condition check is not working and address is not rendered in the generated document .
Could the experts here please suggest how do I go about such conditional check ?
First, you can't have <xsl:when> as a child of anything other than <xsl:choose>. I suspect in this case you probably meant to use <xsl:if>.
Secondly, your template matches core, not the primary address element- Not sure if this is intentional or not though.
Thirdly, your template doesn't actually output anything from the source document anyway, unless there's some missing from what you've included here.
As a rule it's generally advisable to write templates with predicates that fit your conditions, rather than explicit conditional logic within a template. I think what you probably want to do is:
<xsl:template match="core[memberId='0']/address[type='PRIMARY']">
<fo:etc..
</xsl:template>

XSLT: Can a node be a variable and used elsewhere?

xsl
<xsl:variable name="varName>
<xsl:value-of select="/can/be/a/long/path/down/xml/item[#id=1] />
</xsl:variable>
xml
<xml>
<item id="1" text="Yes">
<item id="2" text="No">
</xml>
use
I was thinking I could use like this:
<xsl:when test="$varName/#text = 'Yes'">
blah
</xsl:when>
but blank space is generated in place of variable. Is this even possible, have a node as a variable and use elsewhere?
<xsl:variable name="varName">
<xsl:value-of select="/can/be/a/long/path/down/xml/item[#id=1]" />
</xsl:variable>
This is one of the most common XSLT errors I see. Usually what people intended is:
<xsl:variable name="varName" select="/can/be/a/long/path/down/xml/item[#id=1]"/>
And most of the time, the code works just fine, except that it's a lot slower than it needs to be. But sometimes the fact that the two constructs are quite different beneath the covers comes back to bite you.
To understand the difference, xsl:variable with a select attribute binds the variable to whatever the select expression evaluates to, which in this case is a set of zero or more item elements. By contrast, xsl:variable with nested instructions creates a document node (XSLT 2.0) or result tree fragment (XSLT 1.0) whose content is a COPY of whatever those instructions produce. In this case, because the content is an xsl:value-of instruction, the variable contains a copy of the string-value of the selected node.
And of course, the string value of the selected node doesn't have any attributes, so test="$varname/#text = 'x'" will always return false.

Using same data element name within for-each

More for reference than actual need: what is the XPath syntax to allow me to reference an element in a xsl:for-each block when the same element name is used elsewhere?
Please note, unfortunately this must be a 1.0 solution
For example, I have the following simple XML, and I want to match up the items with the same id value...
<data>
<block1>
<item><id>1</id><text>Hello</text></item>
<item><id>2</id><text>World</text></item>
</block1>
<block2>
<item><id>1</id><text>123</text></item>
<item><id>2</id><text>ABC</text></item>
</block2>
</data>
If I have a for-each on the block1, how can I reference both the id within the block1 and the id within the block2?
This will work, but I think it is messy...
<xsl:for-each select="//block1/item">
<xsl:variable name="id" select="id"/>
<xsl:value-of select="text"/> - <xsl:value-of select="//block2/item[id=$id]/text"/>
</xsl:for-each>
With the result of...
Hello - 123
World - ABC
Is there a simplified way of replacing the $id in select="//block2/item[id=$id]/text" so that it is referring to the id element from the for-each?
Another way to do it which you may find clearer, and will probably be faster, is to use keys:
<xsl:key name="b2" match="block2/item" use="id"/>
then
<xsl:value-of select="key('b2', id)/text"/>
What you have is correct and common as it is. There's no need to simplify it further; it's a standard idiom recognized and used by those working with XSLT.

Creating a variable number of nodes on target document without corresponding data on source document

I'm trying to map two documents witht the BizTalk Mapper and my target document should look like this:
<root>
<complexType>
<property>example</property>
</complexType>
<filler>
<padding>9999999</padding>
</filler>
<filler>
<padding>9999999</padding>
</filler>
<filler>
<padding>9999999</padding>
</filler>
</root>
The number of <filler> nodes that I should create is variable (from 0 to 9). It is basically the result of a calculation (based on some data provided in the source document).
Is there a way to create those <filler> nodes with some combination of functoids?
I tried to use the Table Looping functoid (created a table with only one column, the padding char '9') but doesn't really work because it creates as many <filler> nodes as rows are defined in the table, which is not what I want since the number of rows would have to be variable (again, based on a calculation).
What I currently do is pass the message (XmlDocument) to a C# method and then I programmatically append the <filler> nodes.
I'm hoping that there is a more "BizTalk-y" way of doing this with the Mapper.
I suspect that you will have to solve this problem by altering the XSLT.
Add some logic to create as many filler nodes as the result of your calculation dictates - you could create a template which you call in a loop perhaps, which would append a new filler section.
Hope this points you in the right direction.
As pointed out, XSLT can create nodes on the target document at will (I didn't know this and this was the key part).
Turns out that what I needed is a simple for-loop in XSLT. Once I realized this, a quick Google search yielded the following results:
http://quomon.com/question-How-to-make-a-for-loop-in-xslt-not-for-each-809.aspx
http://snippets.dzone.com/posts/show/930
Another thing worth noting is that (as pointed out by the first link), XSLT is a functional language, not procedural, so sometimes you do have to resort to using recursion or an extension.
This case is definitely one of those times since I couldn't use a careful selection of nodes using the select attribute on an xsl:for-each (since this filler data wasn't part of the source document).
Specifically, for this case, what I did was:
Add a Scripting functoid.
Add two inputs:
A constant with value "1" (this is the initial value of the i variable)
The length of the loop (number of times to repeat the body of the loop)
Paste the following XSLT template as an "Inline XSLT Call Template" script:
<xsl:template name="ForLoop">
<xsl:param name="i" /> <!-- index counter, 1-based, will be incremented with every recursive call -->
<xsl:param name="length" /> <!-- exit loop when i >= length -->
<!-- Output the desired node(s) if we're still looping -->
<!-- The base case is when i > length (in that case, do nothing) -->
<xsl:if test="$i <= $length">
<Filler>
<Padding>999999</Padding>
</Filler>
</xsl:if>
<!-- Call the ForLoop template recursively, incrementing i -->
<xsl:if test="$i <= $length">
<xsl:call-template name="ForLoop">
<xsl:with-param name="i">
<xsl:value-of select="$i + 1"/>
</xsl:with-param>
<xsl:with-param name="length">
<xsl:value-of select="$length"/>
</xsl:with-param>
</xsl:call-template>
</xsl:if>
</xsl:template>