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</开发者_C百科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>
精彩评论