I am trying to cross-reference from an external XML file, but instead of comparing just one key, I want to ask if one string AND other strings exist, and if yes reference from the external file:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:t="http://www.tei-
c.org/ns/1.0"
xmlns="http://www.tei-c.org/ns/1.0" exclude-result-prefixes="xs t">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:param name="ids"
select="document('instructions.xml')"/>
<xsl:key name="id" match="row" use="tokenize(normalize-space(elem[#name='Instruction']), ' ')"/>
<!-- identity transform -->
<xsl:template match="#* | node() | text() | *">
<xsl:copy>
<xsl:apply-templates select="#* | node() | text() | *"/>
</xsl:copy>
</xsl:template>
<xsl:template match="instruction">
<xsl:for-each select=".[contains(.,key('id', ., .))]">
<xsl:copy>
<xsl:attribute name="norm">
<xsl:value-of select="normalize-space(key('id', normalize-space(.), $ids)/elem[#name='Norm'])"/>
</xsl:attribute>
<xsl:apply-templates select="#* | node() | text() | *"/>
</xsl:copy>
</xsl:for-each>
</xsl:template>
Input (External File):
<row>
<elem name="instruction">pour out</elem>
<elem name="norm">p1</elem>
</row>
Input (File to annotate):
<ab type="recipe">
Bla bla
<instruction>pour the milk out</instruction> bla
</ab>
Desired Output:
<ab type="recipe">
Bla bla
<instruction norm="p1">pour the milk out</instruction> bla
</ab>
In order words: Both of the tokens in the external XML file within the element <elem name="instruction"> "pour" AND "out" need to be contained within the <instruction>element in my XML file. If they are I want to set the norm attribute to the value of <elem name="norm"> in the external file.
Any help much appreciated!
I couldn't work out how to do it with a key, but I did come up with an alternate approach....
<xsl:template match="instruction">
<xsl:variable name="words" select="tokenize(normalize-space(.), ' ')" />
<xsl:variable name="row" select="$ids//row[every $i in tokenize(normalize-space(elem[#name='instruction']), ' ') satisfies $i = $words]" />
<xsl:copy>
<xsl:if test="$row">
<xsl:attribute name="norm" select="$row/elem[#name='norm']" />
</xsl:if>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
EDIT: In response to your comment, if you can have multiple rows matching, then to get the one with the most matching words, do this....
<xsl:template match="instruction">
<xsl:variable name="words" select="tokenize(normalize-space(.), ' ')" />
<xsl:variable name="row" as="element()*">
<xsl:perform-sort select="$ids//row[every $i in tokenize(normalize-space(elem[#name='instruction']), ' ') satisfies $i = $words]">
<xsl:sort select="count(tokenize(normalize-space(elem[#name='instruction']), ' '))" order="descending" />
</xsl:perform-sort>
</xsl:variable>
<xsl:copy>
<xsl:if test="$row">
<xsl:attribute name="norm" select="$row[1]/elem[#name='norm']" />
</xsl:if>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
Related
I'm struggling to get this abomination called XSLT to work. I need to get an EXACT attribute at EXACT path, pass its original value to a template and rewrite this value with the result from the template.
I'm having a file like this:
<?xml version="1.0" encoding="windows-1251"?>
<File>
<Document ReportYear="17">
...
...
</Document>
</File>
So I made an XSLT like this:
<?xml version="1.0" encoding="windows-1251"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
<xsl:output method="xml" encoding="windows-1251" indent="yes" />
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()" />
</xsl:copy>
</xsl:template>
<xsl:template name="formatYear">
<xsl:param name="year" />
<xsl:value-of select="$year + 2000" />
</xsl:template>
<xsl:template match="File/Document">
<xsl:copy>
<xsl:apply-templates select="#*" />
<xsl:attribute name="ReportYear">
<xsl:call-template name="formatYear">
<xsl:with-param name="year" select="#ReportYear" />
</xsl:call-template>
</xsl:attribute>
</xsl:copy>
<xsl:apply-templates />
</xsl:template>
</xsl:stylesheet>
This works fine except it closes the <Document> tag immediately and places its content immediately after itself.
Also, can I address the ReportYear attribute value without repeating it twice? I tried current() but it didn't work.
If you're closing <xsl:copy> before applying templates to the remainder of the content of <Document>, then of course <Document> will be closed before the remainder of the content of <Document> appears in the output.
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
exclude-result-prefixes="msxsl"
>
<xsl:output method="xml" encoding="windows-1251" indent="yes" />
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()" />
</xsl:copy>
</xsl:template>
<xsl:template match="Document">
<xsl:copy>
<xsl:apply-templates select="#*" />
<xsl:attribute name="ReportYear">
<xsl:value-of select="#ReportYear + 2000" />
</xsl:attribute>
<xsl:apply-templates select="node()" />
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
outputs
<?xml version="1.0" encoding="windows-1251"?>
<File>
<Document ReportYear="2017">
...
...
</Document>
</File>
I don't think an extra template just for adding 2000 to #ReportYear is necessary. But if you must, you can streamline the whole thing like so
<xsl:template name="formatYear">
<xsl:param name="year" select="#ReportYear" /> <!-- you can define a default value -->
<xsl:value-of select="$year + 2000" />
</xsl:template>
and
<xsl:attribute name="ReportYear">
<xsl:call-template name="formatYear" /> <!-- ...and can use it implicitly here -->
</xsl:attribute>
If you need to process the contents of the Document element with apply-templates and want to keep the result of the applied templates as the children then you need to move the apply-templates inside of the copy:
<xsl:template match="File/Document">
<xsl:copy>
<xsl:apply-templates select="#*"/>
<xsl:attribute name="ReportYear">
<xsl:call-template name="formatYear">
<xsl:with-param name="year" select="#ReportYear"/>
</xsl:call-template>
</xsl:attribute>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
Not sure why you haven't simply used
<xsl:template match="File/Document/#ReportYear">
<xsl:attribute name="{name()}">
<xsl:value-of select=". + 2000"/>
</xsl:attribute>
</xsl:template>
together with the identity transformation template.
Can you set the position of a particular item in a for-each loop if the value equals something? I tried the below example but it didn't work:
<xsl:choose>
<xsl:when test='name = "Dining"'>
<xsl:value-of select="position()=1"/>
</xsl:when>
<xsl:otherwise>
[Normal position]
</xsl:otherwise>
</xsl:choose>
Dining will always appear at the top of the list and then the list will render as normal.
You haven't provided an example of your input XML, or shown exactly what you want to do with it, so I am guessing a bit. You could try something 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="yes"/>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="root">
<xsl:copy>
<xsl:apply-templates select="Dining"/>
<xsl:apply-templates select="*[not(self::Dining)]"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
When applied to the following XML:
<root>
<Bathroom />
<Dining />
<Kitchen />
<Bedroom />
</root>
It produces:
<root>
<Dining />
<Bathroom />
<Kitchen />
<Bedroom />
</root>
I am getting stuck at a point where I need to remove an element from the input XML:
<message
xmlns="http://www.origoservices.com"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
>
<m_control>
<control_timestamp>2013-06-06T14:55:37</control_timestamp>
<initiator_id>ASL</initiator_id>
</m_control>
<m_content>
<b_control>
<quote_type>Single Company</quote_type>
<quote_or_print>Quote And Print</quote_or_print>
<generic_quote_ind>Yes</generic_quote_ind>
<tpsdata>
<tps_quote_type>Comparison</tps_quote_type>
</tpsdata>
</b_control>
<application>
<product>
<tpsdata>
<service_type>QuickQuote</service_type>
<quote_type>Standard</quote_type>
</tpsdata>
</product>
</application>
</m_content>
</message>
if <tps_quote_type> is 'Comparison' then change the value of <quote_type> to 'Comparison' and the <tpsdata> field should be removed. The output should look like below.
<message
xmlns="http://www.origoservices.com"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
>
<m_control>
<control_timestamp>2013-06-06T14:55:37</control_timestamp>
<initiator_id>ASL</initiator_id>
</m_control>
<m_content>
<b_control>
<quote_type>Comparison</quote_type>
<quote_or_print>Quote And Print</quote_or_print>
<generic_quote_ind>Yes</generic_quote_ind>
</b_control>
<application>
<product>
<tpsdata>
<service_type>QuickQuote</service_type>
<quote_type>Standard</quote_type>
</tpsdata>
</product>
</application>
</m_content>
</message>
So far I have tried this XSLT, but I don't know how to remove <tpsdata> field from the output. Could anyone help me in this?
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:dp="http://www.datapower.com/extensions"
xmlns:fn="http://www.w3.org/2005/xpath-functions"
xmlns:date="http://exslt.org/dates-and-times"
extension-element-prefixes="dp"
>
<xsl:output method="xml" indent="yes"/>
<xsl:template match="*">
<!-- identity with closing tags -->
<xsl:element name="{name()}">
<xsl:apply-templates select="#*|node()"/>
</xsl:element>
</xsl:template>
<xsl:variable name="quoteType">
<xsl:value-of select="/*[namespace-uri()='http://www.origoservices.com' and local- name()='message']/*[namespace-uri()='http://www.origoservices.com' and local-name() ='m_content']/*[namespace-uri()='http://www.origoservices.com' and local-name()='b_control']/*[namespace-uri()='http://www.origoservices.com' and local-name()='quote_type']"/>
</xsl:variable>
<xsl:variable name="tpsQuoteType">
<xsl:value-of select="/*[namespace-uri()='http://www.origoservices.com' and local-name()='message']/*[namespace-uri()='http://www.origoservices.com' and local-name()='m_content']/*[namespace-uri()='http://www.origoservices.com' and local-name()='b_control']/*[namespace-uri()='http://www.origoservices.com' and local-name()='tpsdata']/*[namespace-uri()='http://www.origoservices.com' and local-name()='tps_quote_type']"/>
</xsl:variable>
<xsl:template match="/*[namespace-uri()='http://www.origoservices.com' and local-name()='message']/*[namespace-uri()='http://www.origoservices.com' and local-name()='m_content']/*[namespace-uri()='http://www.origoservices.com' and local-name()='b_control']/*[namespace-uri()='http://www.origoservices.com' and local-name()='quote_type']">
<xsl:choose>
<xsl:when test="$tpsQuoteType = 'Comparison' ">
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:text>Comparison</xsl:text>
</xsl:copy>
</xsl:when>
<xsl:otherwise>
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="*|comment()|processing-instruction()">
<xsl:copy>
<xsl:copy-of select="#*|namespace::*"/>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Maybe you noticed that your handling of those elements with a namespace is a little painful. Just add the http://www.origoservices.com namespace to your XSLT and the pain goes away.
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:o="http://www.origoservices.com"
xmlns:dp="http://www.datapower.com/extensions"
xmlns:fn="http://www.w3.org/2005/xpath-functions"
xmlns:date="http://exslt.org/dates-and-times"
extension-element-prefixes="dp"
exclude-result-prefixes="fn date"
>
<xsl:output method="xml" indent="yes"/>
<xsl:template match="node() | #*">
<xsl:copy>
<xsl:apply-templates select="node() | #*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="o:b_control/o:quote_type[../o:tpsdata/o:tps_quote_type = 'Comparison']">
<xsl:copy>
<xsl:apply-templates select="#*" />
<xsl:text>Comparison</xsl:text>
</xsl:copy>
</xsl:template>
<xsl:template match="o:tpsdata[o:tps_quote_type = 'Comparison']" />
</xsl:stylesheet>
Notes
Most of your "plumbing" is not necessary.
Template match expressions don't need to be a full path.
Use match expressions rather than <xsl:choose> to pinpoint elements you want to change.
Start with a basic identity template, overriding it as needed with more specific templates. This makes your live much easier than starting with a modified identity template.
Use empty templates to remove specific elements.
<xsl:stylesheet version="1.0" extension-element-prefixes="dp" exclude-result-prefixes="dp regexp fn dpconfig" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:dp="http://www.datapower.com/extensions" xmlns:dpconfig="http://www.datapower.com/param/config" xmlns:dpfunc="http://www.datapower.com/extensions/functions" xmlns:fn="http://www.w3.org/2005/xpath-functions" xmlns:regexp="http://exslt.org/regular-expressions" >
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="*[local-name()='tpsdata']/*[local-name()='quote_type']">
<xsl:message dp:priority="debug"> Found quote_type </xsl:message>
<xsl:variable name = "First">
<xsl:value-of select="/*[local-name()='message']/*[local-name()='m_content']/*[local-name()='b_control']/*[local-name()='tpsdata']/*[local-name()='tps_quote_type']/text()"/>
</xsl:variable>
<xsl:variable name = "Second">
<xsl:value-of select = "."/>
</xsl:variable>
<xsl:message dp:priority="debug"> Second:<xsl:value-of select = "$Second"/></xsl:message>
<xsl:message dp:priority="debug"> First: <xsl:value-of select = "$First"/> </xsl:message>
<xsl:choose>
<xsl:when test="$Second = $First">
<xsl:message dp:priority="debug"> Stand and Comp are same </xsl:message>
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:when>
<xsl:otherwise>
<xsl:message dp:priority="debug"> Stand and Comp are different </xsl:message>
<xsl:copy>
<xsl:value-of select="regexp:replace(*[local-name()='quote_type'],'','',$First)"/>
</xsl:copy>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="*[local-name()='b_control']/*[local-name()='tpsdata']"/>
</xsl:stylesheet>
I have a document that I need to transform such that most elements are copied as-is, with some exceptions: for certain specified nodes, child elements need to be appended, and some of these child elements need to reference back to specific elements in the source document. A separate "model/crosswalk" xml file contains the elements to add. The elements in the crosswalk that need to refer back to the source document have "source" attributes that point them to specific elements. Of course, my actual source docs (& the actual crosswalk) are more complex and varied than these examples.
Here is a source document example:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<album>
<artist>Frank Sinatra</artist>
<title>Greatest Hits</title>
</album>
<album>
<artist>Miles Davis</artist>
<title>Kind Of Blue</title>
</album>
<movie>
<title>ET</title>
<director>Steven Spielberg</director>
</movie>
<movie>
<title>Blues Brothers</title>
<director>John Landis</director>
</movie>
</root>
Here is the "crosswalk" (crswlk.xml):
<?xml version="1.0" encoding="UTF-8"?>
<root>
<album>
<artist-info>
<artist2 source="artist"/>
</artist-info>
</album>
<movie>
<director-info>
<director2 source="director"/>
</director-info>
</movie>
</root>
And here is the desired output:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<album>
<artist>Frank Sinatra</artist>
<title>Greatest Hits</title>
<artist-info>
<artist2>Frank Sinatra</artist2>
</artist-info>
</album>
<album>
<artist>Miles Davis</artist>
<title>Kind Of Blue</title>
<artist-info>
<artist2>Miles Davis</artist2>
</artist-info>
</album>
<movie>
<title>ET</title>
<director>Steven Spielberg</director>
<director-info>
<director2>Steven Spielberg</director2>
</director-info>
</movie>
<movie>
<title>Blues Brothers</title>
<director>John Landis</director>
<director-info>
<director2>John Landis</director2>
</director-info>
</movie>
</root>
Here's my xslt:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="crosswalk" select="document('crswlk.xml')"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="*/album|movie">
<xsl:variable name="theNode" select="."/>
<xsl:variable name="nodeName" select="name()"/>
<xsl:element name="{$nodeName}">
<xsl:apply-templates select="#* | node()"/>
<xsl:apply-templates select="$crosswalk//*[name()=$nodeName]/*">
<xsl:with-param name="curNode" select="$theNode"/>
</xsl:apply-templates>
</xsl:element>
</xsl:template>
<xsl:template match="*[#source]">
<xsl:param name="curNode" />
<xsl:variable name="sourceNodeName" select="#source"/>
<xsl:element name="{name()}">
<xsl:value-of select="$curNode//*[name()=$sourceNodeName]"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
This stops processing once that last template tries to access $curNode for the first time. When I run the xslt in Eclipse (Xalan 2.7.1) it throws this error: " java.lang.ClassCastException: org.apache.xpath.objects.XString cannot be cast to org.apache.xpath.objects.XNodeSet".
If I pass a similar nodeset as a parameter to a template that matches nodes from the source document, it works as expected - the nodeset is accessible. However, passing the nodeset to the last template above doesn't work. Is it because the template matches nodes from the external document? I sure don't know. Any help much appreciated, it took me a while just to get to this point. Thanks!
Looks like you need to change 2 things:
add tunnel="yes" to xsl:with-param and xsl:param
remove the apostrophes from '$sourceNodeName' in the predicate of the xsl:value-of
Updated XSLT:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="crosswalk" select="document('crswlk.xml')"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="*/album|movie">
<xsl:variable name="theNode" select="."/>
<xsl:variable name="nodeName" select="name()"/>
<xsl:element name="{$nodeName}">
<xsl:apply-templates select="#* | node()"/>
<xsl:apply-templates select="$crosswalk//*[name()=$nodeName]/*">
<xsl:with-param name="curNode" select="$theNode" tunnel="yes"/>
</xsl:apply-templates>
</xsl:element>
</xsl:template>
<xsl:template match="*[#source]">
<xsl:param name="curNode" tunnel="yes"/>
<xsl:variable name="sourceNodeName" select="#source"/>
<xsl:element name="{name()}">
<xsl:value-of select="$curNode//*[name()=$sourceNodeName]"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
Also, you can remove a few of those extra xsl:variables...
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="crosswalk" select="document('crswlk.xml')"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="album|movie">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
<xsl:apply-templates select="$crosswalk/*/*[name()=current()/name()]/*">
<xsl:with-param name="curNode" select="." tunnel="yes"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
<xsl:template match="*[#source]">
<xsl:param name="curNode" tunnel="yes"/>
<xsl:copy>
<xsl:value-of select="$curNode//*[name()=current()/#source]"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
If you are using XSLT 2.0, then Daniel Haley's answer with tunnelling is surely the way to go. If, however, you are actually using Xalan, and therefore only XSLT 1.0, you need to take a different approach.
The problems start on this line:
<xsl:apply-templates select="$crosswalk//*[name()=$nodeName]/*">
This will select either artist-info or director-info in your cross walk document, but you have no specific template matching these, so the generic identity template you are using will match them
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
But this does not take any parameter, not does it pass any parameters on. Therefore, when your last template <xsl:template match="*[#source]"> is matched, the curNode will be empty (an empty string), which is not a node-set, and that saddens Xalan.
So, to solve this in XSLT1.0, just add the parameter to the identity template, and pass it on:
<xsl:template match="node()|#*">
<xsl:param name="curNode" />
<xsl:copy>
<xsl:apply-templates select="#* | node()">
<xsl:with-param name="curNode" select="$curNode" />
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
If the template is ever matched when there is no parameter being passed, it will just pass on an empty parameter without any issue.
Here is the full XSLT (also with the correction of apostrophes being removed from the xsl:value-of, as mentioned in Daniel's answer).
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="crosswalk" select="document('crswlk.xml')"/>
<xsl:template match="node()|#*">
<xsl:param name="curNode" />
<xsl:copy>
<xsl:apply-templates select="#* | node()">
<xsl:with-param name="curNode" select="$curNode" />
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
<xsl:template match="*/album|movie">
<xsl:variable name="theNode" select="."/>
<xsl:variable name="nodeName" select="name()"/>
<xsl:element name="{$nodeName}">
<xsl:apply-templates select="#* | node()"/>
<xsl:apply-templates select="$crosswalk//*[name()=$nodeName]/*">
<xsl:with-param name="curNode" select="$theNode"/>
</xsl:apply-templates>
</xsl:element>
</xsl:template>
<xsl:template match="*[#source]">
<xsl:param name="curNode" />
<xsl:variable name="sourceNodeName" select="#source"/>
<xsl:element name="{name()}">
<xsl:value-of select="$curNode//*[name()=$sourceNodeName]"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
When applied to your XML documents, the following is output
<root>
<album>
<artist>Frank Sinatra</artist>
<title>Greatest Hits</title>
<artist-info>
<artist2>Frank Sinatra</artist2>
</artist-info>
</album>
<album>
<artist>Miles Davis</artist>
<title>Kind Of Blue</title>
<artist-info>
<artist2>Miles Davis</artist2>
</artist-info>
</album>
<movie>
<title>ET</title>
<director>Steven Spielberg</director>
<director-info>
<director2>Steven Spielberg</director2>
</director-info>
</movie>
<movie>
<title>Blues Brothers</title>
<director>John Landis</director>
<director-info>
<director2>John Landis</director2>
</director-info>
</movie>
</root>
I want to transform
<entry>
<parent1>
<object_id>1580</object_id>
</parent1>
<parent1>
<object_id>1586</object_id>
</parent1>
<parent2>
<object_id>1582</object_id>
</parent2>
<parent2>
<object_id>1592</object_id>
</parent2>
</entry>
into
<entry>
<parent1>1580-1586</parent1>
<parent2>1582-1592</parent2>
</entry>
Top-level entry name is unknown. Parent names are unknown, and the number of parent nodes with the same name can vary.
Child nodes are known "object_id".
So, I would like to group the unknown parents in an abstract way, and concatenate child node values, delimited by "-".
Merge XML nodes using XSLT comes close to answering the question, as does Group/merge childs of same nodes in xml/xslt , but they're not quite what I need.
So far I have:
<xsl:key name="groupName" match="*[object_id]" use="."/>
<xsl:template match="*[generate-id(.) = generate-id(key('groupName', .))]">
<xsl:copy>
<xsl:call-template name="join">
<xsl:with-param name="list" select="object_id" />
<xsl:with-param name="separator" select="'-'" />
</xsl:call-template>
</xsl:copy>
</xsl:template>
<xsl:template name="join">
<xsl:param name="list" />
<xsl:param name="separator"/>
<xsl:for-each select="$list">
<xsl:value-of select="." />
<xsl:if test="position() != last()">
<xsl:value-of select="$separator" />
</xsl:if>
</xsl:for-each>
</xsl:template>
Thanks in advance!
Here is a slightly different solution, developed before I noticed Dimitre's post.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:strip-space elements="*" />
<xsl:key name="kParents" match="*[object_id]" use="local-name()" />
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()" />
</xsl:copy>
</xsl:template>
<xsl:template match="*[*/object_id]">
<xsl:variable name="grandparent-id" select="generate-id()" />
<xsl:copy>
<xsl:apply-templates select="#* | node()[not(object_id)] |
*[generate-id()=
generate-id(
key('kParents',local-name())[generate-id(..)=$grandparent-id][1])]"
mode="group-head" />
</xsl:copy>
</xsl:template>
<xsl:template match="*[object_id]" mode="group-head">
<xsl:variable name="grandparent-id" select="generate-id(..)" />
<xsl:copy>
<xsl:apply-templates select="#* | node()[not(self::object_id)]" />
<xsl:for-each select="key('kParents',local-name())[generate-id(..)=$grandparent-id]/object_id">
<xsl:value-of select="." />
<xsl:if test="position() != last()"> - </xsl:if>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Update
I updated the style-sheet to reflect the OP's comment about '-' being a delimiter, rather that a separator between first and last values.
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:key name="kObjByValAndParent" match="object_id"
use="name(..)"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="/*/*"/>
<xsl:template priority="2" match=
"/*/*[generate-id(object_id)
=
generate-id(key('kObjByValAndParent',name())[1])
]
">
<xsl:copy>
<xsl:value-of select=
"concat(object_id, ' - ',
key('kObjByValAndParent',name())[last()]
)
"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
when applied on the provided XML document:
<entry>
<parent1>
<object_id>1580</object_id>
</parent1>
<parent1>
<object_id>1586</object_id>
</parent1>
<parent2>
<object_id>1582</object_id>
</parent2>
<parent2>
<object_id>1592</object_id>
</parent2>
</entry>
produces the wanted, correct result:
<entry>
<parent1>1580 - 1586</parent1>
<parent2>1582 - 1592</parent2>
</entry>
Explanation:
Proper use and overriding of the identity rule.
Proper use of the Muenchian grouping method.
II. In case all values must be concatenated together, use this slightly modified solution:
<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="kObjByValAndParent" match="object_id"
use="name(..)"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="/*/*"/>
<xsl:template priority="2" match=
"/*/*[generate-id(object_id)
=
generate-id(key('kObjByValAndParent',name())[1])
]
">
<xsl:copy>
<xsl:for-each select="key('kObjByValAndParent',name())">
<xsl:if test="not(position()=1)"> - </xsl:if>
<xsl:value-of select="."/>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>