XSLT: move grouped nodes to parent node - xslt-1.0

I have the following XML:
<data>
<request method="PUT">
<context>
<record_count>5</record_count>
<record_type>Customer</record_type>
</context>
<value>
<company>ABC</company>
<customer>Contoso</customer>
</value>
<value>
<company>ABC</company>
<customer>Forest</customer>
</value>
<value>
<company>XYZ</company>
<customer>Forest</customer>
</value>
<value>
<company>XYZ</company>
<customer>Cave</customer>
</value>
<value>
<company>ABC</company>
<customer>Cave</customer>
</value>
</request>
</data>
and the following transformation:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="company-key" match="value" use="company"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*" />
<xsl:apply-templates />
</xsl:copy>
</xsl:template>
<xsl:template match="/data/request/value" />
<xsl:template match="/data/request/value[count(. | key('company-key', company)[1]) = 1]">
<xsl:element name="group">
<xsl:attribute name="company">
<xsl:value-of select="company"/>
</xsl:attribute>
<xsl:attribute name="method">
<xsl:value-of select="/data/request/#method"/>
</xsl:attribute>
<xsl:attribute name="record-type">
<xsl:value-of select="/data/request/context/record_type"/>
</xsl:attribute>
<xsl:for-each select="key('company-key', company)">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:for-each>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
The transformation groups the data in the value nodes into new nodes.
The name of these nodes is group and they have their own attributes.
This works correctly and the ouput looks like this:
<data>
<request method="PUT">
<context>
<record_count>5</record_count>
<record_type>Customer</record_type>
</context>
<group company="ABC" method="PUT" record-type="Customer">
<value>
<company>ABC</company>
<customer>Contoso</customer>
</value>
<value>
<company>ABC</company>
<customer>Forest</customer>
</value>
<value>
<company>ABC</company>
<customer>Cave</customer>
</value>
</group>
<group company="XYZ" method="PUT" record-type="Customer">
<value>
<company>XYZ</company>
<customer>Forest</customer>
</value>
<value>
<company>XYZ</company>
<customer>Cave</customer>
</value>
</group>
</request>
</data>
The problem I have is that I cannot create a transformation that prepares the data as described and then moves all the "group" nodes to parent "data" node and removes the old "request" node.
All this in one transformation.
The output should look like this:
<data>
<group company="ABC" method="PUT" record-type="Customer">
<value>
<company>ABC</company>
<customer>Contoso</customer>
</value>
<value>
<company>ABC</company>
<customer>Forest</customer>
</value>
<value>
<company>ABC</company>
<customer>Cave</customer>
</value>
</group>
<group company="XYZ" method="PUT" record-type="Customer">
<value>
<company>XYZ</company>
<customer>Forest</customer>
</value>
<value>
<company>XYZ</company>
<customer>Cave</customer>
</value>
</group>
</data>

How about simply:
XSLT 1.0
<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:strip-space elements="*"/>
<xsl:key name="company-key" match="value" use="company"/>
<xsl:template match="/data">
<xsl:variable name="method" select="request/#method" />
<xsl:variable name="record_type" select="request/context/record_type" />
<data>
<xsl:for-each select="request/value[count(. | key('company-key', company)[1]) = 1]">
<group company="{company}" method="{$method}" record-type="{$record_type}">
<xsl:copy-of select="key('company-key', company)"/>
</group>
</xsl:for-each>
</data>
</xsl:template>
</xsl:stylesheet>
Note that this assumes that data contains only one request.

Related

Advanced muenchian grouping: group by items in child collection

I'm familiar with simple muenchian grouping in XSL, but I've encountered a problem, which I honestly don't even know, how to approach it.
So I've got an XML:
<whiskies>
<whisky name="ABC" percent="" region="" type="">
<tastingNotesCollection/>
<bottles>
<bottle price="" size="" level="0" date=""/>
<bottle price="" size="" level="70" date=""/>
<bottle price="" size="" level="100" date=""/>
</bottles>
<comments/>
</whisky>
<whisky name="DEF" percent="" region="" type="">
<tastingNotesCollection/>
<bottles>
<bottle price="" size="" level="0" date=""/>
<bottle price="" size="" level="100" date=""/>
</bottles>
<comments/>
</whisky>
<whisky name="GHI" percent="" region="" type="">
<tastingNotesCollection/>
<bottles>
<bottle price="" size="" level="30" date=""/>
</bottles>
<comments/>
</whisky>
<whiskies>
And the goal is to group the whiskies by levels of a bottle:
So level="0" is considered empty.
Anything from level="1" to level="99" is considered open.
And level="100" is considered unopened.
So the transformed result (will be done in HTML) should look like this:
<h1>Empty</h1>
<ul>
<li>ABC</li>
<li>DEF</li>
</ul>
<h1>Open</h1>
<ul>
<li>ABC</li>
<li>GHI</li>
</ul>
<h1>Unopened</h1>
<ul>
<li>ABC</li>
<li>DEF</li>
</ul>
As you can see, the same whisky can show up in multiple groups, depending on how much bottles there are and how full those bottles are.
The second problem is, that the "Open" group doesn't have an exact value and can be aynthing from 1 to 99.
So yeah, don't really know if this can be solved at all or how to even start on this. Any tips appreciated.
I don't think you want to use Muenchian grouping for this. You would need to define a key that enumerates all values in the range from 1 to 99 - and I believe that would take away any advantage that using a key would otherwise bring.
Since your result has either exactly or at most 3 groups (your question is ambiguous in this respect), you could do simply:
XSLT 1.0
<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="/whiskies">
<output>
<group status="empty">
<xsl:for-each select="whisky[bottles/bottle/#level=0]">
<name>
<xsl:value-of select="#name"/>
</name>
</xsl:for-each>
</group>
<group status="open">
<xsl:for-each select="whisky[bottles/bottle[#level>0 and #level < 100]]">
<name>
<xsl:value-of select="#name"/>
</name>
</xsl:for-each>
</group>
<group status="unopened">
<xsl:for-each select="whisky[bottles/bottle/#level=100]">
<name>
<xsl:value-of select="#name"/>
</name>
</xsl:for-each>
</group>
</output>
</xsl:template>
</xsl:stylesheet>
to get (after fixing the input to be a well-formed XML!):
Result
<?xml version="1.0" encoding="utf-8"?>
<output>
<group status="empty">
<name>ABC</name>
<name>DEF</name>
</group>
<group status="open">
<name>ABC</name>
<name>GHI</name>
</group>
<group status="unopened">
<name>ABC</name>
<name>DEF</name>
</group>
</output>
Make your own adjustment for HTML output.
Added:
Here is an alternative approach using keys (though still not Muenchian grouping) which might be more performant:
<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:key name="w" match="whisky" use="bottles/bottle/#level" />
<xsl:key name="w1" match="whisky" use="boolean(bottles/bottle[#level!=0 and #level!=100])" />
<xsl:template match="/whiskies">
<output>
<group status="empty">
<xsl:for-each select="key('w', 0)">
<name>
<xsl:value-of select="#name"/>
</name>
</xsl:for-each>
</group>
<group status="open">
<xsl:for-each select="key('w1', true())">
<name>
<xsl:value-of select="#name"/>
</name>
</xsl:for-each>
</group>
<group status="unopened">
<xsl:for-each select="key('w', 100)">
<name>
<xsl:value-of select="#name"/>
</name>
</xsl:for-each>
</group>
</output>
</xsl:template>
</xsl:stylesheet>
Or even:
<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:key name="bottle" match="bottle" use="ceiling(#level div 99)" />
<xsl:template match="/whiskies">
<output>
<group status="empty">
<xsl:for-each select="key('bottle', 0)/../..">
<name>
<xsl:value-of select="#name"/>
</name>
</xsl:for-each>
</group>
<group status="open">
<xsl:for-each select="key('bottle', 1)/../..">
<name>
<xsl:value-of select="#name"/>
</name>
</xsl:for-each>
</group>
<group status="unopened">
<xsl:for-each select="key('bottle', 2)/../..">
<name>
<xsl:value-of select="#name"/>
</name>
</xsl:for-each>
</group>
</output>
</xsl:template>
</xsl:stylesheet>
If you did want to use xsl:key, you could create 3 of them with the filter criteria:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes" />
<xsl:key name="Empty" match="whisky[bottles/bottle/#level=0]" use="#name"/>
<xsl:key name="Open" match="whisky[bottles/bottle/#level[. > 1 and . < 99]]" use="#name"/>
<xsl:key name="Unopened" match="whisky[bottles/bottle/#level=100]" use="#name"/>
<xsl:template match="/">
<xsl:variable name="whiskies" select="/whiskies/whisky/#name"/>
<xsl:for-each select="document('')/xsl:stylesheet/xsl:key/#name">
<xsl:variable name="status" select="."/>
<h1><xsl:value-of select="$status"/></h1>
<ul>
<xsl:for-each select="$whiskies[key($status, .)]">
<li><xsl:value-of select="."/></li>
</xsl:for-each>
</ul>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

XSLT 1.0 grouping on one or multiple levels

Today's challenge was grouping in XSLT 1.0. Found out there are something called keys and the Muenchian grouping.
Input XML:
<Items>
<Item>
<ID>1</ID>
<Name>A</Name>
<Country>Sweden</Country>
<Region>Småland</Region>
</Item>
<Item>
<ID>2</ID>
<Name>B</Name>
<Country>Sweden</Country>
<Region>Norrland</Region>
</Item>
<Item>
<ID>3</ID>
<Name>C</Name>
<Country>USA</Country>
<Region>Alaska</Region>
</Item>
<Item>
<ID>4</ID>
<Name>D</Name>
<Country>USA</Country>
<Region>Texas</Region>
</Item>
<Item>
<ID>5</ID>
<Name>E</Name>
<Country>Sweden</Country>
<Region>Norrland</Region>
</Item>
</Items>
I need to make thins XML into a better structure, and from this sample XML I't like to get items structured by country and region. Below is wanted result where country and region gets sorted as well:
<Items>
<Country Name="Sweden">
<Region Name="Norrland">
<Item>
<ID>2</ID>
<Name>B</Name>
</Item>
<Item>
<ID>5</ID>
<Name>E</Name>
</Item>
</Region>
<Region Name="Småland">
<Item>
<ID>1</ID>
<Name>A</Name>
</Item>
</Region>
</Country>
<Country Name="USA">
<Region Name="Alaska">
<Item>
<ID>3</ID>
<Name>C</Name>
</Item>
</Region>
<Region Name="Texas">
<Item>
<ID>4</ID>
<Name>D</Name>
</Item>
</Region>
</Country>
</Items>
EDIT:
I also want to make sure regions end up in their own country, even if there are duplicates. I edited the answer accordingly.
Also, I'd like to hint about xsltfiddle.liberty-development.net as an easy way of doing trial-and-error XSLT development...
Inspired by this article, I found a neat solution to this problem:
I have included comments for using it for single or double grouping, see comments in the code. Notice how I use first key (index) as input to the secon for-each loop:
<?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:strip-space elements="*"/>
<xsl:key name="country" match="Item" use="Country" />
<xsl:key name="region" match="Item" use="concat(Region, '|', Country)" />
<xsl:template match="/Items">
<Items>
<xsl:for-each select="Item[generate-id(.) = generate-id(key('country', Country))]">
<xsl:sort select="Country" />
<xsl:variable name="_country" select="Country" />
<xsl:element name="Country">
<xsl:attribute name="Name"><xsl:value-of select="$_country" /></xsl:attribute>
<!-- single level grouping -->
<!--<xsl:apply-templates select="key('country', Country)" />-->
<!-- double grouping -->
<!-- START -->
<xsl:for-each select="key('country', Country)[generate-id(.) = generate-id(key('region', concat(Region, '|', Country)))]">
<xsl:sort select="Region" />
<xsl:variable name="_region" select="Region" />
<xsl:element name="Region">
<xsl:attribute name="Name"><xsl:value-of select="$_region" /></xsl:attribute>
<xsl:apply-templates select="key('region', concat(Region, '|', Country))" />
</xsl:element>
</xsl:for-each>
<!-- END -->
</xsl:element>
</xsl:for-each>
</Items>
</xsl:template>
<xsl:template match="Item">
<xsl:element name="Item">
<xsl:element name="ID"><xsl:value-of select="ID" /></xsl:element>
<xsl:element name="Name"><xsl:value-of select="Name" /></xsl:element>
</xsl:element>
</xsl:template>
</xsl:stylesheet>

xsl key match to return specific value

input:
<DS>
<TL>
<msg>
<output_getquerydata>
<queries>
<query name="q1">
<parameters>
<parameter name="id">906OREA</parameter>
</parameters>
<results>
<record>
<column name="actionState">sdss</column>
</record>
</results>
</query>
<query name="q2">
<parameters>
<parameter name="resCode">CTL</parameter>
<parameter name="prodCode">89CMID</parameter>
<parameter name="pos">1,2,4,3</parameter>
</parameters>
<results>
<record id="1">
<column name="position">1</column>
<column name="ExternalProductId"/>
</record>
<record id="9">
<column name="position"/>
<column name="ExternalProductId">316442</column>
</record>
</results>
</query>
<query name="q2">
<parameters>
<parameter name="resCode">CTL</parameter>
<parameter name="prodCode">91VPRM</parameter>
<parameter name="pos">1,2,4,3</parameter>
</parameters>
<results>
<record id="1">
<column name="position"/>
<column name="ExternalProductId">316495</column>
</record>
</results>
</query>
</queries>
</output_getquerydata>
</msg>
<TL>
<ArticleNr>89CMID</ArticleNr>
</TL>
<TL>
<ArticleNr>89CMID</ArticleNr>
</TL>
<TL>
<ArticleNr>89CMID</ArticleNr>
</TL>
<TL>
<ArticleNr>91VPRM</ArticleNr>
</TL>
</TL>
</DS>
XSL:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="Article" match="tLoading" use="./ArticleNr"/>
<xsl:key name="prod" match="query[#name='q2']/results/record" use="./column[#name='ExternalProductId'][node()]"/>
<xsl:template match="DS">
<msglist>
<xsl:for-each select="./TL[./msg/output_getquerydata/queries/query/results/record/column[#name='actionState'] !='finished'] ">
<xsl:variable name="distinctArticle" select="//TL[string(ArticleNr)][count(. | key('Article',ArticleNr)[1]) = 1]"/>
<msg>
<xsl:for-each select="$distinctArticle">
<load-part>
<!--I need here the value from column[#name='ExtPR'], that has parameter[#name='prodCode']=the current TL articleNr node.
-->
<productId>
<xsl:value-of select="key('prod',column[#name='ExternalProductId'])"/>
</productId>
<!--something-->
</load-part>
</xsl:for-each>
</msg>
</xsl:for-each>
</msglist>
</xsl:template>
</xsl:stylesheet>
Desired OUTPUT:
<msglist>
<msg>
<load-part>
<productId>316442</productId>
</load-part>
<load-part>
<productId>316442</productId>
</load-part>
<load-part>
<productId>316442</productId>
</load-part>
<load-part>
<productId>316495</productId>
</load-part>
</msg>
</msglist>
I need in the productID node, the value from column[#name='ExternalProductId'], that has parameter[#name='prodCode']=the current <TL><ArticleNr> node.
I know that this 'for each' code that I've put, returns only two values, because i'm searching for the distinct values, so I was thinking that I will try with a key, but i'm not sure what I am missing.
Thank you
edited for the correct output values
Firstly, your Article key looks wrong (in the context of your question) as there are no tLoading elements in your XML. So it should be this...
<xsl:key name="Article" match="TL" use="ArticleNr"/>
But to answer your direct question, you need to define your prod key like so
<xsl:key name="prod"
match="query[#name='q2']/results/record"
use="../../parameters/parameter[#name='prodCode']"/>
Then, to look it up, do this...
<xsl:value-of select="key('prod', ArticleNr)/column[#name='ExternalProductId']"/>
Or maybe this, as they "q2" query has two ExternalProductIds...
<xsl:value-of select="key('prod', ArticleNr)/column[#name='ExternalProductId'][. != '']"/>
Try this XSLT (which retains your distinct check, and so only outputs two rows)
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" />
<xsl:key name="Article" match="TL" use="ArticleNr"/>
<xsl:key name="prod" match="query[#name='q2']/results/record" use="../../parameters/parameter[#name='prodCode']"/>
<xsl:template match="DS">
<msglist>
<xsl:for-each select="./TL[./msg/output_getquerydata/queries/query/results/record/column[#name='actionState'] !='finished'] ">
<xsl:variable name="distinctArticle" select="//TL[string(ArticleNr)][count(. | key('Article',ArticleNr)[1]) = 1]"/>
<msg>
<xsl:for-each select="$distinctArticle">
<load-part>
<productId>
<xsl:value-of select="ArticleNr" />
<xsl:text> - </xsl:text>
<xsl:value-of select="key('prod', ArticleNr)/column[#name='ExternalProductId'][. != '']"/>
</productId>
</load-part>
</xsl:for-each>
</msg>
</xsl:for-each>
</msglist>
</xsl:template>
</xsl:stylesheet>

XSLT: Convert Name/Value pair and transform an XML

I need to convert a name value pair into XML. I'm able to generate an XML, but the element name should be grouped and it should not be duplicated. Please see below. The FieldValue element contains 2 OrderItem values in the Detail node. If the FieldValue with OrderItem repeats, then the result should be grouped into one OrderItem node. Please help.
Source XML:
<SC>
<Header>
<Record>
<FieldName>Schema</FieldName>
<FieldValue>OrderHeader</FieldValue>
</Record>
<Record>
<FieldName>Order</FieldName>
<FieldValue>1234</FieldValue>
</Record>
</Header>
<Detail>
<Record>
<FieldName>Schema</FieldName>
<FieldValue>OrderItem</FieldValue>
</Record>
<Record>
<FieldName>Item</FieldName>
<FieldValue>1</FieldValue>
</Record>
<Record>
<FieldName>Qty</FieldName>
<FieldValue>10</FieldValue>
</Record>
</Detail>
<Detail>
<Record>
<FieldName>Schema</FieldName>
<FieldValue>OrderItem</FieldValue>
</Record>
<Record>
<FieldName>Item</FieldName>
<FieldValue>2</FieldValue>
</Record>
<Record>
<FieldName>Qty</FieldName>
<FieldValue>20</FieldValue>
</Record>
</Detail>
</SC>
Target XML:
<Order>
<OrderItem>
<Item>
<Item>1</Item>
<Qty>10</Qty>
</Item>
<Item>
<Item>2</Item>
<Qty>20</Qty>
</Item>
</OrderItem>
</Order>
XSLT:
<xsl:template match="#*|node()">
<Order>
<xsl:for-each select="Detail">
<Item>
<xsl:apply-templates select="Record[position()>1]"/>
</Item>
</xsl:for-each>
</Order>
</xsl:template>
<xsl:template match="Record">
<xsl:element name="{FieldName}">
<xsl:value-of select="FieldValue"/>
</xsl:element>
</xsl:template>
The grouping can be done as follows:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs"
version="2.0">
<xsl:output indent="yes"/>
<xsl:template match="SC">
<Order>
<xsl:for-each-group select="Detail" group-by="Record[1]/FieldValue">
<xsl:element name="{current-grouping-key()}">
<xsl:apply-templates select="current-group()"/>
</xsl:element>
</xsl:for-each-group>
</Order>
</xsl:template>
<xsl:template match="Detail">
<Item>
<xsl:apply-templates select="Record[position() gt 1]"/>
</Item>
</xsl:template>
<xsl:template match="Record">
<xsl:element name="{FieldName}">
<xsl:value-of select="FieldValue"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
It appears that you are trying to define two XSLT templates, when one should be sufficient. You want to match on the root and then that you want to iterate over each SC/Detail.
Then, you want to take the FieldValue of the sibling of the FieldName node that is 'Item' (for item value) and 'Qty' (for quantity value), but only those listed under 'Record'.
Note: You have specified a doubly-nested <Item> in your transformed output and this solution reflects that requirement.
This XSLT should do what you are requesting:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/">
<xsl:for-each select="SC/Detail">
<Order>
<OrderItem>
<Item>
<Item>
<xsl:value-of select="Record[FieldName[text()='Item']]/FieldValue" />
</Item>
<Qty>
<xsl:value-of select="Record[FieldName[text()='Qty']]/FieldValue" />
</Qty>
</Item>
</OrderItem>
</Order>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

Optimization of XSLT

I'm relatively new to XSL and am attempting to elegantly transform a Google Calendar feed into something more readable.
I would appreciate your eyes on whether there are optimizations to be made. In particular, I would like your advice on template use. I've read a lot about how for-each is not appropriate to use willy-nilly (rather, one should attempt to make judicious use of templates).
Thank you very much.
Original XML (showing only one event):
<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:gCal='http://schemas.google.com/gCal/2005' xmlns:gd='http://schemas.google.com/g/2005'>
<id>http://www.google.com/calendar/feeds/bachya1208%40gmail.com/public/full</id>
<updated>2011-09-19T21:32:50.000Z</updated>
<category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/g/2005#event'/>
<title type='text'>John Doe</title>
<subtitle type='text'>John Doe</subtitle>
<link rel='alternate' type='text/html' href='https://www.google.com/calendar/embed?src=bachya1208#gmail.com'/>
<link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='https://www.google.com/calendar/feeds/johndoe%40gmail.com/public/full'/>
<link rel='http://schemas.google.com/g/2005#batch' type='application/atom+xml' href='https://www.google.com/calendar/feeds/johndoe%40gmail.com/public/full/batch'/>
<link rel='self' type='application/atom+xml' href='https://www.google.com/calendar/feeds/johndoe%40gmail.com/public/full?max-results=25'/>
<link rel='next' type='application/atom+xml' href='https://www.google.com/calendar/feeds/johndoe%40gmail.com/public/full?start-index=26&max-results=25'/>
<author>
<name>John Doe</name>
<email>johndoe#gmail.com</email>
</author>
<generator version='1.0' uri='http://www.google.com/calendar'>Google Calendar</generator>
<openSearch:totalResults>1334</openSearch:totalResults>
<openSearch:startIndex>1</openSearch:startIndex>
<openSearch:itemsPerPage>25</openSearch:itemsPerPage>
<gCal:timezone value='America/Denver'/>
<gCal:timesCleaned value='0'/>
<entry>
<id>http://www.google.com/calendar/feeds/johndoe%40gmail.com/public/full/lp0upnpndnkp0ruqht7ef84kds</id>
<published>2011-09-14T21:15:16.000Z</published>
<updated>2011-09-14T21:15:16.000Z</updated>
<category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/g/2005#event'/>
<title type='text'>Oil Change</title>
<content type='text'/>
<link rel='alternate' type='text/html' href='https://www.google.com/calendar/event?eid=bHAwdXBucG5kbmtwMHJ1cWh0N2VmODRrZHMgYmFjaHlhMTIwOEBt' title='alternate'/>
<link rel='self' type='application/atom+xml' href='https://www.google.com/calendar/feeds/johndoe%40gmail.com/public/full/lp0upnpndnkp0ruqht7ef84kds'/>
<author>
<name>John Doe</name>
<email>johndoe#gmail.com</email>
</author>
<gd:comments>
<gd:feedLink href='https://www.google.com/calendar/feeds/johndoe%40gmail.com/public/full/lp0upnpndnkp0ruqht7ef84kds/comments'/>
</gd:comments>
<gd:eventStatus value='http://schemas.google.com/g/2005#event.confirmed'/>
<gd:where valueString='9955 E Arapahoe Road, Englewood, CO 80112 (Go Subaru Arapahoe)'/>
<gd:who email='johndoe#gmail.com' rel='http://schemas.google.com/g/2005#event.organizer' valueString='bachya1208#gmail.com'/>
<gd:when endTime='2011-09-29T11:30:00.000-06:00' startTime='2011-09-29T10:30:00.000-06:00'/>
<gd:transparency value='http://schemas.google.com/g/2005#event.opaque'/>
<gCal:anyoneCanAddSelf value='false'/>
<gCal:guestsCanInviteOthers value='true'/>
<gCal:guestsCanModify value='false'/>
<gCal:guestsCanSeeGuests value='true'/>
<gCal:sequence value='0'/>
<gCal:uid value='lp0upnpndnkp0ruqht7ef84kds#google.com'/>
</entry>
</feed>
XSLT:
<?xml version="1.0"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:template name="formatDateTime">
<xsl:param name="dateTime" />
<xsl:value-of select="concat(substring-before($dateTime, 'T'), ' ', substring-before(substring-after($dateTime, 'T'), '.'))" />
</xsl:template>
<xsl:template match="/">
<Events>
<xsl:apply-templates select="/*/*[local-name()= 'entry']" />
</Events>
</xsl:template>
<xsl:template match="*[local-name()= 'entry']">
<xsl:variable name="startDateTime" select="*[name() = 'gd:when']/#*[local-name() = 'startTime']" />
<xsl:variable name="endDateTime" select="*[name() = 'gd:when']/#*[local-name() = 'endTime']" />
<Event>
<EventTitle>
<xsl:value-of select="*[local-name() = 'title'][1]" />
</EventTitle>
<StartDateTime>
<xsl:call-template name="formatDateTime">
<xsl:with-param name="dateTime" select="$startDateTime" />
</xsl:call-template>
</StartDateTime>
<EndDateTime>
<xsl:call-template name="formatDateTime">
<xsl:with-param name="dateTime" select="$endDateTime" />
</xsl:call-template>
</EndDateTime>
<Who>
<xsl:value-of select="*[local-name() = 'author']/*[local-name() = 'name']" />
</Who>
<Where>
<xsl:value-of select="*[name() = 'gd:where']/#*[local-name() = 'valueString']" />
</Where>
<Status>
<xsl:value-of select="*[name() = 'gd:eventStatus']/#*[local-name() = 'value']" />
</Status>
</Event>
</xsl:template>
</xsl:stylesheet>
Output:
<?xml version="1.0" encoding="UTF-16"?>
<Events>
<Event>
<EventTitle>Oil Change</EventTitle>
<StartDateTime>2011-09-29 10:30:00</StartDateTime>
<EndDateTime>2011-09-29 11:30:00</EndDateTime>
<Who>John Doe</Who>
<Where>9955 E Arapahoe Road, Englewood, CO 80112 (Go Subaru Arapahoe)</Where>
<Status>http://schemas.google.com/g/2005#event.confirmed</Status>
</Event>
</Events>
Your approach looks fine to me. I think your XPath code would be much cleaner and would probably run faster if you used regular element selection instead of local-name. The reason you probably struggled with your XPath was because you're consuming XML that has a default namespace of http://www.w3.org/2005/Atom, and that namespace isn't declared in your stylesheet. Here's a snippet of how a more simplified stylesheet could look, using an f: prefix for the feed namespace:
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:f="http://www.w3.org/2005/Atom"
xmlns:gd="http://schemas.google.com/g/2005">
<!-- ... -->
<xsl:template match="/">
<Events>
<xsl:apply-templates select="//f:entry" />
</Events>
</xsl:template>
<xsl:template match="f:entry">
<xsl:variable name="startDateTime" select="gd:when/#startTime" />
<xsl:variable name="endDateTime" select="gd:when/#endTime" />
<Event>
<EventTitle>
<xsl:value-of select="f:title[1]" />
</EventTitle>
<StartDateTime>
<xsl:call-template name="formatDateTime">
<xsl:with-param name="dateTime" select="$startDateTime" />
</xsl:call-template>
</StartDateTime>
<EndDateTime>
<xsl:call-template name="formatDateTime">
<xsl:with-param name="dateTime" select="$endDateTime" />
</xsl:call-template>
</EndDateTime>
<Who>
<xsl:value-of select="f:author/f:name" />
</Who>
<Where>
<xsl:value-of select="gd:where/#valueString" />
</Where>
<Status>
<xsl:value-of select="gd:eventStatus/#value" />
</Status>
</Event>
</xsl:template>
<!-- etc -->
</xsl:stylesheet>
I would be inclined to replace the formatDateTime template with a match template:
<xsl:template match="#*" mode="formatDateTime">
<xsl:value-of select="concat(substring-before(., 'T'),
' ', substring-before(substring-after(., 'T'), '.'))" />
</xsl:template>
and change the calls to
<StartDateTime>
<xsl:apply-templates select="$startDateTime" mode="formatDateTime"/>
</StartDateTime>
<EndDateTime>
<xsl:apply-templates select="$endDateTime" mode="formatDateTime"/>
</EndDateTime>
Just because the call-template syntax is so verbose.
(and I would probably inline the variables too - they don't see to add value).
Here is a complete transformation that is derived from the provided, solving the default namespace problem (as already done by #Jacob), but also completely eliminating the unnecessary template matching the document node (/) and assuring that two unwanted namespaces will not appear on every (literal result) element in the output:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:a="http://www.w3.org/2005/Atom"
xmlns:gd="http://schemas.google.com/g/2005"
exclude-result-prefixes="a gd">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="a:entry[1]">
<Events>
<xsl:apply-templates select="../a:entry" mode="process"/>
</Events>
</xsl:template>
<xsl:template match="a:entry" mode="process">
<xsl:variable name="startDateTime" select="gd:when/#startTime" />
<xsl:variable name="endDateTime" select="gd:when/#endTime" />
<Event>
<EventTitle>
<xsl:value-of select="a:title[1]" />
</EventTitle>
<StartDateTime>
<xsl:call-template name="formatDateTime">
<xsl:with-param name="dateTime" select="$startDateTime" />
</xsl:call-template>
</StartDateTime>
<EndDateTime>
<xsl:call-template name="formatDateTime">
<xsl:with-param name="dateTime" select="$endDateTime" />
</xsl:call-template>
</EndDateTime>
<Who>
<xsl:value-of select="a:author/a:name" />
</Who>
<Where>
<xsl:value-of select="gd:where/#valueString" />
</Where>
<Status>
<xsl:value-of select="gd:eventStatus/#value" />
</Status>
</Event>
</xsl:template>
<xsl:template name="formatDateTime">
<xsl:param name="dateTime" />
<xsl:value-of select="concat(substring-before($dateTime, 'T'), ' ', substring-before(substring-after($dateTime, 'T'), '.'))" />
</xsl:template>
<xsl:template match="text()|a:entry"/>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:gCal='http://schemas.google.com/gCal/2005' xmlns:gd='http://schemas.google.com/g/2005'>
<id>http://www.google.com/calendar/feeds/bachya1208%40gmail.com/public/full</id>
<updated>2011-09-19T21:32:50.000Z</updated>
<category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/g/2005#event'/>
<title type='text'>John Doe</title>
<subtitle type='text'>John Doe</subtitle>
<link rel='alternate' type='text/html' href='https://www.google.com/calendar/embed?src=bachya1208#gmail.com'/>
<link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='https://www.google.com/calendar/feeds/johndoe%40gmail.com/public/full'/>
<link rel='http://schemas.google.com/g/2005#batch' type='application/atom+xml' href='https://www.google.com/calendar/feeds/johndoe%40gmail.com/public/full/batch'/>
<link rel='self' type='application/atom+xml' href='https://www.google.com/calendar/feeds/johndoe%40gmail.com/public/full?max-results=25'/>
<link rel='next' type='application/atom+xml' href='https://www.google.com/calendar/feeds/johndoe%40gmail.com/public/full?start-index=26&max-results=25'/>
<author>
<name>John Doe</name>
<email>johndoe#gmail.com</email>
</author>
<generator version='1.0' uri='http://www.google.com/calendar'>Google Calendar</generator>
<openSearch:totalResults>1334</openSearch:totalResults>
<openSearch:startIndex>1</openSearch:startIndex>
<openSearch:itemsPerPage>25</openSearch:itemsPerPage>
<gCal:timezone value='America/Denver'/>
<gCal:timesCleaned value='0'/>
<entry>
<id>http://www.google.com/calendar/feeds/johndoe%40gmail.com/public/full/lp0upnpndnkp0ruqht7ef84kds</id>
<published>2011-09-14T21:15:16.000Z</published>
<updated>2011-09-14T21:15:16.000Z</updated>
<category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/g/2005#event'/>
<title type='text'>Oil Change</title>
<content type='text'/>
<link rel='alternate' type='text/html' href='https://www.google.com/calendar/event?eid=bHAwdXBucG5kbmtwMHJ1cWh0N2VmODRrZHMgYmFjaHlhMTIwOEBt' title='alternate'/>
<link rel='self' type='application/atom+xml' href='https://www.google.com/calendar/feeds/johndoe%40gmail.com/public/full/lp0upnpndnkp0ruqht7ef84kds'/>
<author>
<name>John Doe</name>
<email>johndoe#gmail.com</email>
</author>
<gd:comments>
<gd:feedLink href='https://www.google.com/calendar/feeds/johndoe%40gmail.com/public/full/lp0upnpndnkp0ruqht7ef84kds/comments'/>
</gd:comments>
<gd:eventStatus value='http://schemas.google.com/g/2005#event.confirmed'/>
<gd:where valueString='9955 E Arapahoe Road, Englewood, CO 80112 (Go Subaru Arapahoe)'/>
<gd:who email='johndoe#gmail.com' rel='http://schemas.google.com/g/2005#event.organizer' valueString='bachya1208#gmail.com'/>
<gd:when endTime='2011-09-29T11:30:00.000-06:00' startTime='2011-09-29T10:30:00.000-06:00'/>
<gd:transparency value='http://schemas.google.com/g/2005#event.opaque'/>
<gCal:anyoneCanAddSelf value='false'/>
<gCal:guestsCanInviteOthers value='true'/>
<gCal:guestsCanModify value='false'/>
<gCal:guestsCanSeeGuests value='true'/>
<gCal:sequence value='0'/>
<gCal:uid value='lp0upnpndnkp0ruqht7ef84kds#google.com'/>
</entry>
</feed>
the wanted, correct result is produced:
<Events>
<Event>
<EventTitle>Oil Change</EventTitle>
<StartDateTime>2011-09-29 10:30:00</StartDateTime>
<EndDateTime>2011-09-29 11:30:00</EndDateTime>
<Who>John Doe</Who>
<Where>9955 E Arapahoe Road, Englewood, CO 80112 (Go Subaru Arapahoe)</Where>
<Status>http://schemas.google.com/g/2005#event.confirmed</Status>
</Event>
</Events>