Find value of XPath in different object in array - xslt

I'm currently building a document in XSL-FO to build a table based on a list of items. The issue is that the items have relationships between them, and I need to be able to reference a value from the other item based on the relationship.
Say I had an input object like:
<Products>
<Product>
<ID>A</ID>
<Name>Cat</Name>
<Relationship>
<ID>B</ID>
</Relationship>
</Product>
<Product>
<ID>B</ID>
<Name>Hat</Name>
</Product>
</Products>
I need to be able to put together a table that has the format of:
Name
----
Cat
- Hat
----
Hat
To build the table rows, I've already done
<fo:table>
<xsl:apply-templates select='Product' />
</fo:table>
and then 'within' each Product, putting a block based on the name:
<fo:block>
<xsl:value-of select="Name" />
</fo:block>
<fo:block>
<xsl:apply-template select="..." />
</fo:block>
My issue is the ... select option to get the name. I was hoping to be able to build an xpath along the lines of ../Product[ID=./Relationship/ID]/Name
but it doesn't work because the ./ now refers to any of the products, not just the "starting" object.
Is there a way to accomplish this referencing using xpath?

XSLT has a built-in key mechanism to resolve cross-references. Start by defining a key at the top level of your stylesheet as:
<xsl:key name="product" match="Product" use="ID" />
Then, from the context of Product, you can do:
<xsl:apply-templates select="key('product, Relationship/ID)/Name"/>
Alternatively, you could do:
<xsl:apply-templates select="../Product[ID=current()/Relationship/ID]/Name"/>
But using a key is both more elegant and more efficient.

Related

hyphenation character in xslt attribute (xsl-fo)

I think it's a very simple question. But although I build very fancy xslt transformation, this simple one cannot be solved by me.
The problem is:
I want to add attributes to xsl-fo nodes, depending on xml data. These attributes have often a hyphen in it. How can I add these with an xslt transformation where xsl:attributes doesn't like the hyphenation character.
In a xml node I have got two attributes (name and value)
Example: name="font_size", value="7pt"
<Report>
<text content="I am a text">
<blockFormat name="font_size" value="7pt" />
</text>
</Report>
(I understand this is not wanted because you want to work with styles etceters. It's just an example with a simplified problem)
Now I want to make a xsl-fo block, and I want to place that attributes in the block element by using the xsl-function xsl:attribute
<fo:block>
<attribute name="{replace(#name,'_','-')}" select="#value" />
....
</fo:block>
goal to achieve after transformation
<fo:block font-size="7pt">
....
</fo:block
It doesn't function and I think this is because in xslt I can't put an hyphen in the attribute name, but in the fo-attribute it is needed.
Is there a way to use the xsl:attribute function for this?
And when not, what kind of working around do you suggest.
Thank you for helping!!!!
There are 1000 ways to do it, here is one (I didn't do anything with your Report element):
Input:
<Report>
<text content="I am a text">
<blockFormat name="font_size" value="7pt" />
</text>
</Report>
XSL:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fo="http://www.w3.org/1999/XSL/Format"
version="1.0">
<xsl:template match="Report">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="text">
<fo:block>
<xsl:apply-templates select="blockFormat/#*"/>
<xsl:value-of select="#content"/>
</fo:block>
</xsl:template>
<xsl:template match="#name">
<xsl:attribute name="{translate(.,'_','-')}">
<xsl:value-of select="ancestor::blockFormat/#value"/>
</xsl:attribute>
</xsl:template>
<xsl:template match="#value"/>
</xsl:stylesheet>
Output:
<Report>
<fo:block xmlns:fo="http://www.w3.org/1999/XSL/Format" font-size="7pt">I am a text</fo:block>
</Report>
Use #select instead of #value:
<fo:block>
<attribute name="{replace(#name,'_','-')}" select="#value" />
....
</fo:block>
See https://www.w3.org/TR/xslt20/#creating-attributes
Also, you need to be using XSLT 2.0 or 3.0 to use #select. If you're using XSLT 1.0, you'd have to do it as xsl:attribute/xsl:value-of/#select.
(It would also have helped understanding of your problem if you'd also shown the wrong result that you were getting.)

How to map 2 different nodes into a repeating node using BizTalk mapper

I have this schema:
<Root>
<Customers>
<Customer>
<ID>123</ID>
<Name>John</Name>
</Customer>
</Customers>
<Order>
<ID>ABC</ID>
<Title>Boat</Title>
</Order>
</Root>
I need to map the two different records into one repeating record like this:
<Root>
<Data>
<ID>123</ID>
<Text>John</Text>
</Data>
<Data>
<ID>ABC</ID>
<Text>Boat</Text>
</Data>
</Root>
I tried to create two table loopings (one for Customer, one for Order) and got:
<Root>
<Data>
<ID>ABC</ID>
<Text>Boat</Text>
</Data>
</Root>
Tried one table looping with two rows, and got the same. (Tried also with the Gated option to check for existance which made no difference)
In reality the schemas are huge, the map is super complex (not built by me), has a lot of functoids, and many wires. So I would like to avoid creating a custom XSL, which will be easier for this task, but harder to maintain. This is the only part I need to change.
Anybody ?
Thanks.
For complex mapping, using a custom XSLT almost always ends up being simpler and more maintainable than the spider-web we often find in BizTalk maps. However, as you stated, you need to avoid re-coding the complete map, as you are only changing a small section.
You should be able to use the 'Inline XSLT Call Template' script type in the Scripting Functoid to combine the best of BizTalk maps and custom XSLT.
Extending from Sean B. Durkin's answer, you will need to set up 2 Call Template functoids, the first one wired to your output 'Data' node
<xsl:template name="DataTemplate">
<xsl:apply-templates select="//*[local-name()='Customer']|//*[local-name()='Order']" />
</xsl:template>
Your second Call Template will output the relevant data into the current output 'Data' node. Note, this second Functoid does not need to be wired to any node in your output document.
<xsl:template match="*[local-name()='Customer']|*[local-name()='Order']">
<xsl:element name="Data">
<xsl:element name="ID">
<xsl:value-of select="*[local-name()='ID']"/>
</xsl:element>
<xsl:element name="Text">
<xsl:value-of select="*[local-name()='Name']|*[local-name()='Title']" />
</xsl:element>
</xsl:element>
</xsl:template>
No need to use XSLT here. Simply drag a Looping functoid on the map. Connect both the Customer and the Order record as inputs to the functoid (yes, you can have multiple inputs). Connect the output of the functoid to the Data record. Then connect your fields directly (ID --> ID, Name --> Text). This will work.
The individual input records to a Looping functoid don't have to be repeating records in themselves. By connecting multiple inputs to the functoid, you are looping over the collection of instances.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<Root>
<xsl:apply-templates select="*/Customers/Customer|*/Order"/>
</Root>
</xsl:template>
<xsl:template match="Customer|Order">
<Data>
<ID><xsl:value-of select="ID" /></ID>
<Text><xsl:value-of select="Name|Title" /></Text>
</Data>
</xsl:template>
</xsl:stylesheet>

Sort list by datatype dropdown list order

How can I sort a list in XSLT in the same order as my datatype drop down list?
I've tried the following without any success:
<xsl:variable name="tarrifs" select="umbraco.library:GetPreValues(1601)//preValue" />
<xsl:sort select="tarrifs" order="descending" />
I have a list of options in my datatype "Tariff Category" and I want to order the list in my XSLT in the same order as my datatype.
Order of my datatype:
UE tariff criteria
UE pricing proposals
UE tariff schedules
UE other charges
UE tariff strategy reports
MG tariff schedules
MG tariff reports
MG ancillary and other charges
To start with, <xsl:sort... must appear inside either a <xsl:apply-templates... or <xsl:for-each... tag.
To sort with subjective logic, like you need, there's a couple of approaches you can take. The simplest is probably this:
XML
<root>
<item>dog</item>
<item>cat</item>
<item>horse</item>
<item>dragonfly</item>
</root>
XSL
<!-- this sheet vars -->
<xsl:variable name='sort_order' select='"dragonfly|horse|dog|cat"' />
<!-- root and static content -->
<xsl:template match="/">
<xsl:apply-templates select='root/item'>
<xsl:sort select='string-length(substring-before($sort_order, current()/text()))' data-type='number' />
</xsl:apply-templates>
</xsl:template>
<!-- iteration content - animal -->
<xsl:template match='item'>
<p><xsl:value-of select='.' /></p>
</xsl:template>
You can test it out at this XMLPlayground session.
The concept, as you can probably see, is to declare the desired sort order as a string, then iteratively sort our nodes based on the position of each node's text value within that sort string.
Are you looking for something like this:-
Assuming your xml is this way:-
XML:
<preValues>
<preValue id="19">Value 1</preValue>
<preValue id="20">Value 2</preValue>
<preValue id="21">Value 3</preValue>
</preValues>
XSLT:
If you want to sort by id in descending order then do this way
<xsl:for-each select="umbraco.library:GetPreValues(1601)//preValue">
<xsl:sort select="#id" order="descending" />
<!-- Do Your Stuff -->
</xsl:for-each>
Follow the same methodology to sort by multiple attribute values.

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]"/>

How do I retrieve data from an XML document using XSLT for use in an ASP.NET datagrid?

I am have trouble converting my xml using XSLT back to xml in the convert format, this is my XML below:
<?xml version="1.0" encoding="UTF-8" ?>
<DOCUMENTS>
<DOCUMENTS_INFO DOCUMENT_COUNT="8" />
<DOCUMENT_GROUP NAME="Invoices" DISPLAY_NAME="INVOICES">
<DOCUMENT>
<FIELD>
<name>USER</name>
<display_name>Deposited by</display_name>
<value>machine</value>
</FIELD>
<FIELD>
<name>DATE</name>
<display_name>Archive date</display_name>
<value>21/05/2009</value>
</FIELD>
</DOCUMENT>
<DOCUMENT>
<FIELD>
<name>USER</name>
<display_name>Deposited by</display_name>
<value>machine</value>
</FIELD>
<FIELD>
<name>DATE</name>
<display_name>Archive date</display_name>
<value>21/06/2009</value>
</FIELD>
</DOCUMENT>
</DOCUMENT_GROUP>
</DOCUMENTS>
I need to convert so I can read it into my datagrid in ASP.NET so the output would be something like this below:
Deposited by | Archive date
machine | 21/05/2009
machine | 21/06/2009
Many Thanks
You don't really need to do any conversion at all. If you want to pull the data into a dataset or something so that you can display it, all you need is a little bit of XPath.
The XPath to get all DOCUMENT elements:
//DOCUMENT
And the XPath to get a pair of value elements based on the name value, based in a DOCUMENT element:
FIELD[name = 'USER']/value
FIELD[name = 'DATE']/value
So depending on the technology you use to parse the XML, you'll basically need a loop over the first expression, and then run the following two expressions on the results of the first loop. In XSL it'll look something like this:
<xsl:template match="/">
<xsl:for-each select="//DOCUMENT">
<xsl:value-of select="FIELD[name = 'USER']/value"/>
<xsl:text>|</xsl:text>
<xsl:value-of select="FIELD[name = 'DATE']/value"/>
</xsl:for-each>
</xsl:template>
Like anything, there's more than one way to do this. You can use a template as well:
<xsl:template match="/">
<xsl:apply-templates select="//DOCUMENT" />
</xsl:template>
<xsl:template match="DOCUMENT">
<xsl:value-of select="FIELD[name = 'USER']/value"/>
<xsl:text>|</xsl:text>
<xsl:value-of select="FIELD[name = 'DATE']/value"/>
</xsl:template>
Both of these will give the output you gave as an example, but you're probably better off reading the XPath values directly into a dataset or writing your own adapter to pull these values out and load them directly into a datagrid.