Friday, August 04, 2006

Separating breadcrumb (homeward path) navigation from content using XML/XSL

My experiments with SiteMesh got me thinking about how best to separate out breadcrumb like navigation from the core application content. Strictly speaking I mean Homeward Path navigation rather than the form of breadcrumb navigation that essentially gives you an onscreen potted browser history.

I have seen numerous approaches to this kind of navigation. The custom JSP tag libraries for this kind of thing that I have seen have never felt quite right. In my travels I recently discovered something called JATO (also known as Sun Java System Application Framework). JATO seems to have been around for a long while but I hadn't seen it before because it appears that it was only distributed as part of the Sun ONE Application Server (formerly iPlanet Application Server). JATO seems to be a MVC framework that existed before the JSTL, Struts, JSF and Spring era.

One of the examples in the JATO sample application is of a Static Breadcrumb method.

I liked the example but not the implementation. So I thought to myself, I can do better than that! What I have ended up with is a very simple and consequently quite satisfying solution. Using the navigationSample.xml from the JATO sample application, I have achieved the same result through a fairly simple XSL transformation. This is achieved with bog standard JSTL without the need to write any new tag libraries. You could easily extend the navigation xml sitemap to include more link information and if you were using SiteMesh you could also take advantage of any "Where Am I?" information passed from your content pages.

Here is JATO's navigationSample.xml as used in the JATO sample application.


<?xml version="1.0" encoding="UTF-8"?>
<category name="Performing Arts" id="0">
<category name="Acting" id="2">
<category name="Actors" id="20">
<category name="Buster Keaton" id="200"/>
<category name="Charlie Chaplin" id="201"/>
<category name="WC Fields" id="202"/>
</category>
<category name="Actresses" id="21">
<category name="Mae West" id="210"/>
<category name="Bette Davis" id="211"/>
<category name="Marlene Dietrich" id="212"/>
</category>
<category name="Companies" id="22"/>
</category>
<category name="Circus Arts" id="3">
<category name="Acrobatics" id="30">
<category name="Anti-Gravity" id="300"/>
<category name="Human Design" id="301"/>
<category name="Trixy's Arco Page" id="302"/>
</category>
<category name="Clowning" id="31">
<category name="Rodeo Clowns" id="310">
<category name="One Eyed Jack" id="3100"/>
<category name="Cool Nite Rodeo" id="3101"/>
</category>
<category name="Creepy Clowns" id="311">
<category name="Creepy Clown Gallery" id="3110"/>
<category name="Ghost Town Clowns" id="3111"/>
</category>
<category name="Anti-Clowns" id="312">
<category name="The No Clown Zone" id="3120"/>
<category name="The Anti-Clown Page" id="3121"/>
</category>
</category>
</category>
</category>

Here is my simple JSP which currently does the XML/XSL transformation and passes any appropriate parameters into the XSL. It could of cource be optimized for performance, e.g. caching the XSL or XML in memory.


<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%><%--
--%>
<%@taglib uri="http://java.sun.com/jsp/jstl/xml" prefix="x"%><%--
--%>
<c:import url="navigationSample.xml" var="xml"/><%--
--%>
<c:import url="navigation.xsl" var="xsl"/><%--
--%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<style type="text/css">
body {
font-family: helvetica;
font-size: 0.8em;
}
</style>
</head>
<body>
<c:choose><%--
--%>
<c:when test="${empty param.id}"><%--
--%>
<x:transform xml="${xml}" xslt="${xsl}"/><%--
--%>
</c:when><%--
--%>
<c:otherwise><%--
--%>
<x:transform xml="${xml}" xslt="${xsl}"><%--
--%>
<x:param name="id" value="${param.id}"/><%--
--%>
</x:transform><%--
--%>
</c:otherwise><%--
--%>
</c:choose>
</body>
</html>

Here is my navigation.xsl


<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" omit-xml-declaration="yes" />
<xsl:param name="id" select="'0'"/>
<xsl:strip-space elements="category"/>

<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>

<xsl:template match="category[@id = $id]">
<xsl:for-each select="ancestor::category">
<a>
<xsl:attribute name="href">?id=<xsl:value-of select="@id"/></xsl:attribute>
<xsl:value-of select="@name"/>
</a> &gt;
</xsl:for-each>
<a>
<xsl:attribute name="href">?id=<xsl:value-of select="@id"/></xsl:attribute>
<xsl:value-of select="@name"/>
</a>
<xsl:if test="count(category) &gt; 0">
<ol>
<xsl:for-each select="category">
<li>
<a>
<xsl:attribute name="href">?id=<xsl:value-of select="@id"/></xsl:attribute>
<xsl:value-of select="@name"/>
</a>
</li>
</xsl:for-each>
</ol>
</xsl:if>
</xsl:template>

</xsl:stylesheet>

And that is all you need, horribly simple eh? I hope so.

Technically there is nothing stopping us doing XSL transformations for navigation in the client. This emergent navigation seems to be the direction that Web 2.0 applications are going. Although what stops me doing this now is all the cross browser XSL transformation issues and accessibility issues of needing to support non-javascript driven solutions on the client.

3 comments:

Mark McLaren said...

Doesn't work due to [@id=$id] expression
Note: Comment imported. Original by Adrian at 2008-03-07 10:57

Mark McLaren said...

I suppose it depends on your XSLT engine, it works in Xalan. I'm sorry it doesn't work for you Adrian.
Note: Comment imported. Original by markmc website: http://cse-mjmcl.cse.bris.ac.uk at 2008-03-26 19:12

Mark McLaren said...

Hi,

It works with










Note: Comment imported. Original by Claud at 2008-07-09 18:14