Selecting distinct grand-children from XML per node - xslt

After spending a couple of days on trying to find a solution using muenchian grouping I desperately need some help in figuring out the proper xslt code for transforming my XML so that it will produce a unique list of years for all shows (to be imported into Solr eventually). Basically I need the unique grandchildren of a node to be grouped, but somehow all existing solutions on SO did not work for me.
Here's my source XML:
<?xml version="1.0" encoding="ISO-8859-1"?>
<?xml-stylesheet type="text/xsl" href="http://localhost/default.xsl" ?>
<DIRECTORY>
<SHOWS>
<SHOW>
<TITLE>Child's play</TITLE>
<EVENTS>
<EVENT>
<YEAR>2014</YEAR>
<TITLE>Gala day</TITLE>
</EVENT>
<EVENT>
<YEAR>2014</YEAR>
<TITLE>Gala night</TITLE>
</EVENT>
<EVENT>
<YEAR>2015</YEAR>
<TITLE>Gala night</TITLE>
</EVENT>
</EVENTS>
</SHOW>
</SHOWS>
</DIRECTORY>
Here's the desired output:
<?xml version="1.0"?>
<add>
<doc>
<field name="show_title">Child's play</field>
<field name="show_year">2014</field>
<field name="show_year">2015</field>
</doc>
</add>
Here's my XSLT that does not work and is based on various answers around Stack Overflow
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" omit-xml-declaration="no"/>
<xsl:key name="show_key"
match="TITLE"
use="TITLE"/>
<xsl:key name="show_years_key"
match="TITLE"
use="concat(TITLE, ' ',
EVENTS/EVENT/YEAR)"/>
<xsl:template match="/">
<add>
<xsl:for-each select="/DIRECTORY/SHOWS/SHOW">
<doc>
<xsl:call-template name="show_info"/>
</doc>
</xsl:for-each>
</add>
</xsl:template>
<xsl:template name="show_info">
<field name="show_title">
<xsl:value-of select="TITLE"/>
</field>
<xsl:variable
name="show_events"
select="key('show_key', TITLE)"/>
<xsl:for-each
select="$show_events[generate-id() =
generate-id(
key('show_years_key',
concat(TITLE,
' ',
EVENTS/EVENT/YEAR))[1])]">
<field name="show_years">
<xsl:value-of select="EVENTS/EVENT/YEAR"/>
</field>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
While I really appreciate any help to get to a working solution I could also use some pointers as to why my solution does not work when it should. I left the minimal example a bit more complex on purpose as I suspect my xslt organization might make things more complicated, but the overall solution works quite well, even with multiple different templates.

The grouping key needs to match the elements you're trying to group (i.e. the YEAR elements). I'm having a bit of trouble following your call-template logic, I'd approach it more like this:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" omit-xml-declaration="no"/>
<xsl:key name="show_years_key"
match="YEAR"
use="concat(ancestor::SHOW[1]/TITLE, ' ', .)"/>
<xsl:template match="/">
<add>
<xsl:apply-templates select="/DIRECTORY/SHOWS/SHOW" />
</add>
</xsl:template>
<xsl:template match="SHOW">
<doc>
<field name="show_title">
<xsl:value-of select="TITLE"/>
</field>
<!-- current() here is the SHOW element this template applies to -->
<xsl:for-each select="EVENTS/EVENT/YEAR[generate-id() = generate-id(
key('show_years_key', concat(current()/TITLE, ' ', .))[1])]">
<field name="show_years">
<xsl:value-of select="." />
</field>
</xsl:for-each>
</doc>
</xsl:template>
</xsl:stylesheet>
The important point is that you're grouping the YEAR elements by the TITLE of their containing SHOW, not the SHOW elements by the YEAR they contain.

Related

Sorting elements by edited attribute

I am trying to merge two XML files "a.xml" and "b.xml" into a HTML table using XSLT 1.0. Both files contain elements called "event" that each have a "time" attribute with a dateTime value attached to them. I want the HTML table to be sorted chronologically. Whereas the time attributes of file "a.xml" are formated correctly (CCYY-MM-DDTHH:MM:SS.msmsms), the time attributes of "b.xml" are not (CCYY-DDDTHH:MM:SS.msmsmsZ) and thus I am using some concats and substring functions to construct the correct format for the "time" attributes of the "b.xml" elements. My question is now: How can I use the original "time" attributes of "a.xml" and the corrected attributes of "b.xml" for sorting the rows of the HTML table?
I already tried using parameters for storing the correctly formated "time" attributes. I also tried using node-sets to tackle the issue in two steps (i.e. converting "b.xml" attributes first, saving result and then creating the HTML from the intermediate file), but neither of these two ways worked for me. Lastly I tried sorting the HTML table on load of the page with a JavaScript, but the table is too big for doing it this way on each page load.
I am happy about every hint on a functionality of XSLT that could help me. I have to stick with XSLT1.0, though and can't use XSLT2.0 for this project.
a.xml
<?xml version="1.0" encoding="UTF-8"?>
<data>
<event time="2019-02-03T06:00:00.000"></event>
<event time="2019-02-01T06:00:00.000"></event>
</data>
b.xml before formating
<?xml version="1.0" encoding="UTF-8"?>
<data>
<event time="2019-035T06:00:00.000"></event>
<event time="2019-033T06:00:00.000"></event>
</data>
b.xml after formating
<?xml version="1.0" encoding="UTF-8"?>
<data>
<event time="2019-02-04T06:00:00.000"></event>
<event time="2019-02-02T06:00:00.000"></event>
</data>
current transform.xsl
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<html>
<body>
<table>
<xsl:apply-templates select="/Adata/event|document('b.xml')/Bdata/event">
<xsl:sort select="#time"/>
</xsl:apply-templates>
</table>
</body>
</html>
</xsl:template>
<xsl:template match="Bdata/event">
<!--Here I have some long operation to change the date format and save it as parameter "correctFormat"-->
<xsl:attribute name="time">
<xsl:value-of select="$correctFormat"/>
</xsl:attribute>
</xsl:template>
<xsl:template match="//event">
<tr>
<td><xsl:value-of select="#time"/></td>
</tr>
</xsl:template>
</xsl:stylesheet>
expected output out.html
<html>
<body>
<table>
<tr><td>2019-02-01T06:00:00.000</td></tr>
<tr><td>2019-02-02T06:00:00.000</td></tr>
<tr><td>2019-02-03T06:00:00.000</td></tr>
<tr><td>2019-02-04T06:00:00.000</td></tr>
</table>
</body>
</html>
EDIT
Date conversion code
As requested I share as well my code for the conversion. I am using the exslt dates and time namespace by adding inside the header
<xsl:template match="data/event">
<xsl:param name="daysToAdd" select="concat('P',substring(#time,6,3),'D')"/>
<xsl:param name="startOfYear" select="concat(substring(#time,1,4),'-01-00')"/>
<xsl:param name="formatedDate">
<xsl:call-template name="date:add">
<xsl:with-param name="date-time" select="$startOfYear" />
<xsl:with-param name="duration" select="$daysToAdd" />
</xsl:call-template>
<xsl:value-of select="substring(#time,9,13)"/>
</xsl:param>
<xsl:copy>
<xsl:attribute name="time">
<xsl:value-of select="$formatedDate"/>
</xsl:attribute>
</xsl:copy>
</xsl:template>
Consider the following example (minimized to the problem presented in your question):
XML
<data>
<event time="2019-02-03T06:00:00.000"></event>
<event time="2019-02-01T06:00:00.000"></event>
</data>
b.xml
<data>
<event time="2019-035T06:00:00.000"></event>
<event time="2019-033T06:00:00.000"></event>
</data>
XSLT 1.0
<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="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/data">
<!-- CONVERT B -->
<xsl:variable name="b">
<xsl:for-each select="document('b.xml')/data/event">
<xsl:copy>
<xsl:attribute name="time">
<!-- the missing conversion part goes here -->
</xsl:attribute>
</xsl:copy>
</xsl:for-each>
</xsl:variable>
<!-- OUTPUT -->
<xsl:copy>
<xsl:for-each select="event | exsl:node-set($b)/event">
<xsl:sort select="#time" data-type="text" order="ascending"/>
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Result
<?xml version="1.0" encoding="UTF-8"?>
<data>
<event time="2019-02-01T06:00:00.000"/>
<event time="2019-02-02T06:00:00.000"/>
<event time="2019-02-03T06:00:00.000"/>
<event time="2019-02-04T06:00:00.000"/>
</data>

Transformation of text file into an xml using datapower

I want to transfer a non-xml text file delimited by '|' characters into an xml using Datapower.
Following is file (sample1)
10|20003|24/23/25|23890
Now i have to break this into the following XML
<ResponseType>
<ResCode>10</ResCode>
<Id>20003</Id>
<SoftCode>24/23/25</SoftCode>
<StatusCode>23890</StatusCode>
</ResponseType>
What I did was following--
1>Create a Transform action in the service that will be receiving non-XML requests.
2>Select "Use XSLT specified in this action on a non-XML message" to specify that this is a Binary Transform.
3>Upload the following stylesheet as the Processing Control File.
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:dp="http://www.datapower.com/extensions"
version="1.0">
<dp:input-mapping href="sample1.ffd" type="ffd"/>
<xsl:output method="xml"/>
<xsl:template match="/">
<xsl:copy-of select="ResponseType"/>
<xsl:call-template name="str:tokenize">
<xsl:with-param name="string" select="string" />
</xsl:call-template>
</xsl:template>
<xsl:template name="str:tokenize">
<xsl:with-param name="string" select="">
str:tokenize('string', '|')
</xsl:with param>
</xsl:template>
</xsl:stylesheet>
and here is my sample1.ffd(which I have uploaded in my local:// directory in Datapower
<File name="ResponseType">
<!-- capture all data into this tag -->
<Field name="ResCode/Id/SoftCode/StatusCode" />
</File>
But I am not getting desired output , I think my xslt is quite wrong
What can I do do to get desired output?
In DataPower using FFD the following should work:
1) Add the FFD file (below one of my old education samples):
<File name="CSVFILE">
<Group name="CSVLine" minOccurs="0" maxOccurs="unbounded" delim="\n">
<Field name="id"/>
<Field name="fname" delim=","/>
<Field name="lname" delim=","/>
<Field name="title" delim=","/>
<Field name="dept" delim=","/>
<Field name="org"/>
</Group>
</File>
2) Add the XSLT (this one simply copies the FFD transformed XML to output):
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:dp="http://www.datapower.com/extensions"
version="1.0">
<dp:input-mapping href="CSVFILE.FFD" type="ffd"/>
<!-- This stylesheet copies the input to the output -->
<xsl:output method="xml"/>
<xsl:template match="/">
<xsl:copy-of select="." />
</xsl:template>
</xsl:stylesheet>
3) Send a message:
1,Anders,Wasen,B2B Architect,DataPower Dev,Enfo Zystems
2,Jean-Luc,Piccard,Captain,USS Enterprise,Star Fleet
4) This will result in the following XML:
<?xml version="1.0" encoding="UTF-8"?>
<CSVFILE>
<CSVLine>
<id>1</id>
<fname>Anders</fname>
<lname>Wasen</lname>
<title>B2B Architect</title>
<dept>DataPower Dev,Enfo Zystems</dept>
<org/>
</CSVLine>
<CSVLine>
<id>2</id>
<fname>Jean-Luc</fname>
<lname>Piccard</lname>
<title>Captain</title>
<dept>USS Enterprise,Star Fleet</dept>
<org/>
</CSVLine>
</CSVFILE>
Make sure that you change the XSLT Transform action into "Transform Binary" and set Request Type to "non-xml", else it will not work!
Hope this will help you! :)
I'm not sure how IBM Datapower might solve this problem, but for XSLT, you would at least wrap your input in a XML element:
<Whatever>
10|20003|24/23/25|23890
</Whatever>
And then you could go on with a transformation like follows. The hard part is splitting your text input. In XSLT 1.0, there is no function available for that, so you need a recursive template.
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxml="urn:schemas-microsoft-com:xslt" version="1.0" exclude-result-prefixes="msxml">
<xsl:output method="xml" indent="yes" />
<xsl:template match="/">
<xsl:variable name="tokenized">
<items>
<xsl:call-template name="tokenize">
<xsl:with-param name="string" select="//text()" />
</xsl:call-template>
</items>
</xsl:variable>
<ResponseType>
<ResCode>
<xsl:copy-of select="msxml:node-set($tokenized)/items/item[1]/text()" />
</ResCode>
<Id>
<xsl:copy-of select="msxml:node-set($tokenized)/items/item[2]/text()" />
</Id>
<SoftCode>
<xsl:copy-of select="msxml:node-set($tokenized)/items/item[3]/text()" />
</SoftCode>
<StatusCode>
<xsl:copy-of select="msxml:node-set($tokenized)/items/item[4]/text()" />
</StatusCode>
</ResponseType>
</xsl:template>
<xsl:template name="tokenize">
<xsl:param name="string" />
<xsl:variable name="item" select="normalize-space( substring-before( concat( $string, '|'), '|'))" />
<xsl:if test="$item">
<item>
<xsl:value-of select="$item" />
</item>
<xsl:call-template name="tokenize">
<xsl:with-param name="string" select="substring-after($string,'|')" />
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>

XSLT multiple stylesheets

I have the following xml
<TopLevel>
<data m="R263">
<s ut="263firstrecord" lt="2013-02-16T09:21:40.393" />
<s ut="263secondrecord" lt="2013-02-16T09:21:40.393" />
</data>
<data m="R262">
<s ut="262firstrecord" lt="2013-02-16T09:21:40.393" />
<s ut="262secondrecord" lt="2013-02-16T09:21:40.393" />
</data>
</TopLevel>
I have some XSLT that does the call template but it's not itterating correctly.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="data">
<xsl:value-of select="#m" />
<xsl:variable name="vYourName" select="#m"/>
<xsl:choose>
<xsl:when test="#m='R262'">
<xsl:call-template name="R262"/>
</xsl:when>
</xsl:choose>
<xsl:choose>
<xsl:when test="#m='R263'">
<xsl:call-template name="R263"/>
</xsl:when>
</xsl:choose>
</xsl:template>
<xsl:template name="R262">
<xsl:for-each select="/TopLevel/data/s">
Column1=<xsl:value-of select="#ut" />
Column2=<xsl:value-of select="#lt" />
</xsl:for-each>
</xsl:template>
<xsl:template name="R263">
<xsl:for-each select="/TopLevel/data/s">
Column1=<xsl:value-of select="#ut" />
Column2=<xsl:value-of select="#lt" />
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
This gives me 8 records insead of the 4 (<s> level) records. I know it has to do with my iteration ... but I am not sure how to address this.
I am also aware of the apply stylesheets but I couldn't unravel that mystery either ... If someone can help me with XSLT that will only process everything from <TopLevel> to <\TopLevel> checking the value of m at the <data> level and applying the stylesheet at the <s> level for each <s> record I will be greateful beyond belief.
I don't know what output you want to produce, but I suspect you want to replace
<xsl:for-each select="/TopLevel/data/s">
by
<xsl:for-each select="s">
that is, you only want to process the "s" elements within the "data" you are currently processing, rather than selecting all the "s" elements in the whole document.
Why not do this using apply-templates?
<xsl:template match="data">
...
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="s[../#m='R262']">
...
</xsl:template>
<xsl:template match="s[../#m='R263']">
...
</xsl:template>
If you want to use match template and apply-templates you could do the following which gives you also a text output just like your stylesheet does. So this XSLT applied to your original source XML:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="data">
<xsl:value-of select="#m"/>
<xsl:apply-templates select="s"/>
</xsl:template>
<xsl:template match="s">
Column1=<xsl:value-of select="#ut"/>
Column2=<xsl:value-of select="#lt"/>
</xsl:template>
</xsl:stylesheet>
gives you this output:
<?xml version="1.0" encoding="UTF-8"?>
R263
Column1=263firstrecord
Column2=2013-02-16T09:21:40.393
Column1=263secondrecord
Column2=2013-02-16T09:21:40.393
R262
Column1=262firstrecord
Column2=2013-02-16T09:21:40.393
Column1=262secondrecord
Column2=2013-02-16T09:21:40.393
You basically only match on the s and give out the attributes "ut" and "lt". You can also output XML which would look better.
Using this XSLT:
<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/>
</root>
</xsl:template>
<xsl:template match="data">
<list>
<xsl:apply-templates select="s"/>
</list>
</xsl:template>
<xsl:template match="s">
<xsl:element name="record">
<xsl:attribute name="m">
<xsl:value-of select="parent::data/#m"/>
</xsl:attribute>
<item>Column1=<xsl:value-of select="#ut"/></item>
<item>Column2=<xsl:value-of select="#lt"/></item>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
will give you this nice XML output:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<list>
<record m="R263">
<item>Column1=263firstrecord</item>
<item>Column2=2013-02-16T09:21:40.393</item>
</record>
<record m="R263">
<item>Column1=263secondrecord</item>
<item>Column2=2013-02-16T09:21:40.393</item>
</record>
</list>
<list>
<record m="R262">
<item>Column1=262firstrecord</item>
<item>Column2=2013-02-16T09:21:40.393</item>
</record>
<record m="R262">
<item>Column1=262secondrecord</item>
<item>Column2=2013-02-16T09:21:40.393</item>
</record>
</list>
You have to adapt the original XSLT a little bit to get a nice XML structure. Also when matching s you "climb" up to element data to get the R-numbers for your attribute values.
The template matching root you need for a proper XML root element. <list> you could also get rid off then you have <record> as child of <root>.

xpath get matches in sublist

I've got the following xml:
<vo class="GroupEntry" buildByAlias="true">
<objectClass name="groupOfNames"/>
<field name="commonName" nameLDAP="cn" type="String"/>
<field name="descriptione" nameLDAP="description" type="String"/>
<field name="member" nameLDAP="member" type="String[]"/>
</vo>
<update method="addMember" modificationMode="ADD_ATTRIBUTE">
<input>
<field name="member"/>
<field name="description"/>
</input>
</update>
I'm using XSLT to transform it, and I'm need, for each update, to get the fields in the vo that correspond to the field defined in the input. It would be something like this:
<xsl:variable name="fields" select="vo/field" />
<xsl:for-each select="update">
<xsl:variable name='fieldsForInput' select = "$fields[#name]=input/fields[#name]"/>
<xsl:for-each select="$fieldsForInput">
<xsl:value-of select="#type"/> <xsl:value-of select="#name"/>
<xsl:for-each>
</xsl:for-each>
But it doesn't found anything. Any ideas?
Thanks
JL
From the shown fragments it's difficult helping you and understadning what you want. However your case seems perfect for using xsl:key.
For example, if you create a key at the beginning of the transform like this:
<xsl:key name="fields" match="vo/field" use="#name"/>
You can use it inside your matching template as follows:
<xsl:for-each select="update/input">
<xsl:copy-of select="key('fields',current()/field/#name)"/>
</xsl:for-each>
I would not use a xsl:foreach anyway. But it's hard to give you a complete solution if you provide only fragments. Also is not clear if you want just match or replace field.
Example showing how to get the field name/type for each update/input/field.
XSLT 1.0 tested with Saxon 6.5.5
<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:key name="fields" match="vo/field" use="#name"/>
<xsl:template match="/root">
<xsl:apply-templates select="update"/>
</xsl:template>
<xsl:template match="update">
<xsl:value-of select="concat('-',#method,'
')"/>
<xsl:apply-templates select="input/field"/>
</xsl:template>
<xsl:template match="input/field">
<xsl:value-of select="concat('--',#name,' ',key('fields',#name)/#type,'
')"/>
</xsl:template>
</xsl:stylesheet>
Applied on:
<root>
<vo class="GroupEntry" buildByAlias="true">
<objectClass name="groupOfNames"/>
<field name="commonName" nameLDAP="cn" type="String"/>
<field name="description" nameLDAP="description" type="String"/>
<field name="member" nameLDAP="member" type="String[]"/>
</vo>
<update method="addMember" modificationMode="ADD_ATTRIBUTE">
<input>
<field name="member"/>
<field name="description"/>
</input>
</update>
<update method="deleteMember" modificationMode="DELETE_ATTRIBUTE">
<input>
<field name="member"/>
<field name="description"/>
</input>
</update>
</root>
Produces:
-addMember
--member String[]
--description String
-deleteMember
--member String[]
--description String
Two solutions:
Solution1 (no keys):
<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=
"vo/field[#name=../../update/*/*/#name]">
<xsl:value-of select="concat(#name,' ',#type,'
')"/>
</xsl:template>
</xsl:stylesheet>
When applied on the provided XML document (corrected to be made well-formed):
<t>
<vo class="GroupEntry" buildByAlias="true">
<objectClass name="groupOfNames"/>
<field name="commonName" nameLDAP="cn" type="String"/>
<field name="description" nameLDAP="description" type="String"/>
<field name="member" nameLDAP="member" type="String[]"/>
</vo>
<update method="addMember" modificationMode="ADD_ATTRIBUTE">
<input>
<field name="member"/>
<field name="description"/>
</input>
</update>
</t>
the wanted, correct result is produced:
description String
member String[]
Solution2 (using a key):
<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:key name="kFieldByName" match="vo/field"
use="#name"/>
<xsl:template match="/*">
<xsl:apply-templates mode="selected" select=
"key('kFieldByName', update/*/*/#name)"/>
</xsl:template>
<xsl:template match="vo/field" mode="selected">
<xsl:value-of select="concat(#name,' ',#type,'
')"/>
</xsl:template>
</xsl:stylesheet>
when applied on the same XML document (above), the same correct result is produced:
description String
member String[]

Using xslt, transform one xml document based upon the contents of a second xml document

I have one set of documents that implicitly define the allowed fields for a second set of objects that have to be transformed into a third set of documents (
which "rules" document to use depends upon the content of the file being transformed) e.g.
<!-- One example rules document -->
<document object="obj1_rules">
<field name="A"/>
<field name="B"/>
<field name="C"/>
</document>
<!-- Document to be tranformed based upon obj1_rules-->
<document object="obj1">
<field name="A"/>
<field name="B"/>
<field name="C"/>
<field name="D"/>
<field name="E"/>
</document>
<!-- Desired result-->
<document object="obj1">
<newfield name="A"/>
<newfield name="B"/>
<newfield name="C"/>
</document>
Is it possible to do this transformation using xslt?
I see that "There is no way in XSLT of constructing XPath expressions (e.g. variable references) at run-time." So I am out of luck, or I am just looking at this problem incorrectly? Thanks!
Here is a simple solution:
This transformation:
<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:variable name="vrtfRules">
<document object="obj1_rules">
<field name="A"/>
<field name="B"/>
<field name="C"/>
</document>
</xsl:variable>
<!-- -->
<xsl:variable name="vRules" select=
"document('')/*/xsl:variable
[#name = 'vrtfRules']
/*
"/>
<!-- -->
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<!-- -->
<xsl:template match="field">
<xsl:if test="#name = $vRules/*/#name">
<newfield>
<xsl:apply-templates select="node()|#*"/>
</newfield>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
when applied on the originaly-provided source XML document:
<document object="obj1">
<field name="A"/>
<field name="B"/>
<field name="C"/>
<field name="D"/>
<field name="E"/>
</document>
produces the desired result:
<document object="obj1">
<newfield name="A"/>
<newfield name="B"/>
<newfield name="C"/>
</document>
Note that the "rules document" is within the stylesheet just for compactness. When it is a separate document, just the document() function used will need to be adjusted with the actual href.
Maybe I'm oversimplifying, but is there a reason why your "rules document" cannot simply be an XSLT?
Well, I can see why you'd like to have rules in a simple xml file rather than in a full-fledged xsl stylesheet but you're simply skipping a step.
You need to make a xsl stylesheet that will transform your xml rule document into a xsl stylesheet that you will then apply onto your source xml.
The trick is with namespaces and not getting confused by the mix of xsl rules applied and xsl rules generated.
<?xml version="1.0" ?>
<xsl:stylesheet
xmlns="YOUR_NAMESPACE_HERE"
xmlns:output="http://www.w3.org/1999/XSL/Transform"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0">
<xsl:output
method="xml"
indent="yes"
media-type="text/xsl" />
<xsl:template match="/">
<output:stylesheet version="2.0">
<xsl:apply-templates />
</output:stylesheet>
</xsl:template>
<xsl:template match="document[#object]">
<output:template match="document[#object='{#object}']">
<output:copy>
<xsl:apply-templates />
</output:copy>
</output:template>
</xsl:template>
<xsl:template match="field[#name]">
<output:if test="field[#name='{#name}']">
<output:copy-of select="field[#name='{#name}']" />
</output:if>
</xsl:template>
</xsl:stylesheet>
I presumed you'd use the same document object attribute in the rules and in the documents themselves (this is much simpler imo).
So, you run your rules document through the stylesheet above. The result is a new xsl stylesheet that does exactly what you describe in your xml rule document. You then apply this resulting stylesheet onto your source document and you should get the result you expect.