Breadcrumbs framework for Grails

This post describes a simple framework that can be used for implementing breadcrumbs in a Grails application. lets get started…

The three main components of this framework are:

  • Breadcrumb configuration XML which is used to defined breadcrumb hierarchy and attributes.
  • Breadcrumb manager which is used for parsing/loading XML configuration file, finding correct breadcrumb for the page and in general managing breadcrumb implementation
  • Custom tag <g:breadCrumb> which is used to embedded breadcrumbs on pages.

Lets look at breadcrumbs.xml first, I placed this XML in WEB-INF folder of the application right next to applicationContext.xml

breadcrumbs.xml

<!-- Bread crumb configuration XML -->
<breadcrumbs>
	<!-- Mapping is used to define the navigation structure for a bread crumb, it is based on the site map.
		 matchController and matchAction are used to figure location of requested page on the site by matching
		 the controller/action of the request to matchController/matchAction combination. Once a match is found
		 bread crumb hierarchy is resolved and information crumbs tag is used to get details related to each
		 bread crumb. All attributes in <nav> tag are mandatory
	-->
	<map>
		<nav id="homeCrumb" matchController="samplePages" matchAction="homeBreadCrumbPage">
			<!-- levels navigation -->
			<nav id="itemsLevel1Crumb" matchController="samplePages" matchAction="level1BreadCrumbPage">
				<nav id="itemsLevel2Crumb" matchController="samplePages" matchAction="level2BreadCrumbPage">
					<nav id="itemsLevel3Crumb" matchController="samplePages" matchAction="level3BreadCrumbPage">
						<nav id="showItemCrumb" matchController="samplePages" matchAction="itemDetailsBreadCrumbPage"/>
					</nav>
				</nav>
			</nav>
			<nav id="simple1Crumb" matchController="samplePages" matchAction="simpleBreadCrumb"/>
			<nav id="simple2Crumb" matchController="samplePages" matchAction="simpleBreadCrumbWithAttr"/>
			<!-- levels navigation -->
		</nav>
	</map>

		<!-- Information in <crumb> tag is used to figure out detail of individual bread crumb, the navigation bread crumb
			 and details are matched using id attribute. If for a 'id' in navigation map no details bread crumb id is found
			 then the 'id' is ignored.

			  - Optional viewController and viewAction attributes in XML should be supplied together, if any one of them
			  is missing other will be ignored.

			  - viewController/viewAction combination has a higher precedence than viewTemplate, viewTemplate attribute
			  will be ignored if both are present

			  - linkToController/linkToAction combination has a higher precedent than link, link attribute  will be ignored
			  if both are present

			  - Optional linkToController and linkToAction attributes in XML should be supplied together, if any one of them
			  is missing other will be ignored

			  - If rendering of a bread crumb is delegated to action/template then bread crumb title needs to be generated by
				renderer

			  - Optional parameters can be supplied for each id in JSON format i.e. params = { foo: 'bar' , key: 'value'}

			  - Requested page is given an opportunity to define the title/link if the bread crumb by supplying title/link attribute
			    If these are supplied then configured title/link in XML are overridden
		-->
	<crumbs>
		<crumb id="homeCrumb" title="Home" linkToController="samplePages" linkToAction="homeBreadCrumbPage" />
		<crumb id="itemsLevel1Crumb" viewController="breadCrumb" viewAction="renderLevel1Crumb" />
		<crumb id="itemsLevel2Crumb" viewController="breadCrumb" viewAction="renderLevel2Crumb" />
		<crumb id="itemsLevel3Crumb" viewController="breadCrumb" viewAction="renderLevel3Crumb" />
		<crumb id="showItemCrumb" />
		<crumb id="simple1Crumb" title="Simple1"/>
		<crumb id="simple2Crumb"/>
	</crumbs>
</breadcrumbs>

XML file is divided into two sections, first section is used to define hierarchical relationship between the views for which we need to display breadcrumbs. For each element we define matchController/matchAction attributes which are used by our custom tag to figure out its position in breadcrumb hierarchy based on current page. We also define unique ‘id’ attribute for each element, this is used in second section of XML to define properties used while rendering the breadcrumb. Please go though the comments in the XML for more details.

Now lets look at elements that provide breadcrumb manager implementation

The first code element is BreadCrumb.groovy which provides an object for holding properties loaded from XML for each breadcrumb

BreadCrumb.groovy

package org.breadcrumbs

import org.apache.commons.lang.builder.EqualsBuilder
import org.apache.commons.lang.builder.HashCodeBuilder

/**
 * class for handling BreadCrumbs
 * 
 */
class BreadCrumb {
	
	// mandatory bread crumb identifier, checked while parsing XML
	String id
	
	// Title to be used for the Bread Crumb (optional)
	String title
	
	// Controller to be used to render the view (optional)
	String viewController
	
	// Action to be used to render the view (optional)
	String viewAction
	
	// If controller/action is not required, only template can 
	// be supplied for rendering the view (optional)
	String viewTemplate
	
	// link that needs to attached to bread crumb title (optional)
	String link
	
	// Controller that should be used to create link for title (optional)
	String linkToController
	
	// Action that should be used to create link for title (optional)
	String linkToAction
	
	// crumb parameters map, loaded from XML configuration
	def params 
	
	/*
	 * Create a copy of bread crumb instance
	 */
	def getCopy () {
		def paramList = [:]
		for ( e in params ) 
			paramList.put (e.key, e.value)
                 
		return new BreadCrumb(title: this.title, link: this.link, 
				viewController: this.viewController, viewAction: this.viewAction, viewTemplate: this.viewTemplate,
				linkToController: this.linkToController, linkToAction: this.linkToAction, params: paramList)
	}
	
	@Override
	int hashCode () { 
		return new HashCodeBuilder().append(this.id).toHashCode() 
	}
	
	@Override
	boolean equals (Object other) { 
		if(other?.is(this)) 
			return true 
		if(!(other instanceof BreadCrumb)) 
			return false 
		
		return new EqualsBuilder().append(this.id, other.id).isEquals() 
	}
}

Second element is custom exception for breadcrumbs
BreadCrumbException.groovy

package org.breadcrumbs

/**
 * class to handle bread crumb Exceptions 
 */
class BreadCrumbException extends Exception  {
    
	BreadCrumbException() {
		super();
	}
	
	BreadCrumbException(Exception e) {
		super(e);
	}
	
	BreadCrumbException (String text) {
		super (text);
	}
}

Third and last element is BreadCrumbManager.groovy which provides lazy loaded singelton instance as breadcrumb manager
BreadCrumbManager.groovy

package org.breadcrumbs

import org.apache.commons.logging.LogFactory
import grails.converters.XML
import org.codehaus.groovy.grails.commons.ApplicationHolder
import grails.converters.*
import org.codehaus.groovy.grails.web.json.*
import org.codehaus.groovy.grails.web.converters.exceptions.ConverterException

import org.breadcrumbs.BreadCrumbException


/**
 * Singleton class for managing BreadCrumbs
 */
class BreadCrumbManager {
	
	private static def log = LogFactory.getLog("grails.app.BreadCrumbManager")
	
	private static def NAVIGATION_TAG = "nav"
	private static def CRUMB_DETAIL_TAG = "crumb"
	private static def CRUMB_DELIMITER = "."
	private static def ALL_ACTION_WILDCARD = "*"	
	private static def XML_FILE_PATH = "WEB-INF/breadcrumbs.xml" 
	
	// instance is not lazy loading because bread crumbs are required on all pages
	private static BreadCrumbManager manager = new BreadCrumbManager()
	
	// Map stores bread crumb navigation id's for matchers
	private def matcherToCrumbIdsMap 
	// Map stores bread crumb instance for each bread crumb id
	private def crumbIdDetailMap                           
		
	/**
	 * private constructor for creating BreadCrumbManager instance
	 */
	private BreadCrumbManager() {
		matcherToCrumbIdsMap = [:]
		crumbIdDetailMap = [:]
		// load navigation maps and bread crumb attributes from XML instances
		loadBreadCrumbsFromXML()
	}
	
	/**
	 * Return existing BreadCrumb manager instance 
	 */
	static BreadCrumbManager getInstance() {
		return manager
	}
	
	/**
	 * Match and retrieve bread crumbs navigation list 
	 * 
	 * @param  controller: controller to be matched
	 * @param  action: action to be matched
	 * 
	 * @return list of applicable bread crumb instances
	 */
	def getBreadCrumbs (def controller, def action) {
		
		def key = getMatcherKey(controller, action)
		def crumbs = matcherToCrumbIdsMap.get(key)
		
		if (!crumbs) {
			// if id not for for specific controller/action combination, try to get
			// an id applicable to all actions of a controller
			def alternatekey = getMatcherKey(controller, ALL_ACTION_WILDCARD)
			crumbs = matcherToCrumbIdsMap.get(alternatekey)
		}
		
		// log if still not found
		if (!crumbs) {
			log.warn "No matching crumbs for key ${key}" 
		} else {
			log.debug "Matching crumbs for key ${key} : ${crumbs}" 
			
			def crumbArray = crumbs.split('\\.')
			def crumbList = []
			crumbArray.each { crumb ->
				def crumbObj = crumbIdDetailMap.get(crumb)
				
				if (crumbObj) {
					// create a copy of original crumb object and add to list
					crumbList.add (crumbObj.getCopy())
				} else {
					log.info "Crumb details not found for '${crumb}' "
				}
			}
			return crumbList
		}
	}
	
	/**
	 * Construct a matcher key from controller and action  
	 * 
	 * @param  controller: controller to be matched
	 * @param  action: action to be matched
	 * 
	 * @return matcher string
	 */
	def getMatcherKey(def controller, def action) {
		if (controller && action)
			return "${controller.toLowerCase()}/${action.toLowerCase()}"
	}
	
	/**
	 * load navigation map and crumb details from XML  
	 * 
	 */
	private loadBreadCrumbsFromXML () {
		// get application context and resource
		def file = ApplicationHolder.application.getParentContext().getResource(XML_FILE_PATH).getFile()  
		log.debug "Loading breadcrumbs from file " + file.getAbsolutePath()  
		
		def xml = new XmlParser().parse(file)
		xml.depthFirst().each { node ->
			
			if (node.name() == NAVIGATION_TAG) {
					// throw exception if any of the required attributes are missing
					if (!node.@id || !node.@matchController || !node.@matchAction) {
						throw new BreadCrumbException("Required attribute for navigation missing, please validate XML ")
					}	
					
					def key = getMatcherKey(node.@matchController, node.@matchAction)
					
					if (matcherToCrumbIdsMap.get(key)) {
						throw new BreadCrumbException("Duplicate matcher key found: ${key}")
					} else {
						def p = node.parent()
						def crumbs = node.@id
						// move towards top of tree from current node to get crumb hierarchy 	
						while(p?.parent() != null) {
							if (p.name() == NAVIGATION_TAG) {
								crumbs = p.@id + CRUMB_DELIMITER + crumbs
							}
							p = p.parent()
						}
						
						log.debug "Putting matcher key: ${key} for crumb navigation ${crumbs} "
						matcherToCrumbIdsMap.put(key, crumbs.toLowerCase())
					}
			} else {
				if (node.name() == CRUMB_DETAIL_TAG) {
					def id = node.@id
					if (!id) {
						throw new BreadCrumbException("Required attribute 'id' for crumb  missing, please validate XML ")
					}
					
					def crumbParams
					
					if (node.@params) {
						// try parsing params
						try {
								crumbParams = JSON.parse(node.@params) // Parse a JSON param string
						} catch(ConverterException exp) {
							throw new BreadCrumbException("Params: ${node.@params} specified in incorrect format " +
									"for id: '${id}' , please validate XML ")
						}
					}
				
					log.debug "Putting crumb detail for id: ${id}"
					// create a bread crumb object and save it on details map
					crumbIdDetailMap.put(id.toLowerCase(), new BreadCrumb(title: node.@title, link: node.@link, 
							viewController : node.@viewController, viewAction: node.@viewAction, 
							viewTemplate: node.@viewTemplate, linkToController: node.@linkToController,
							linkToAction: node.@linkToAction, params: crumbParams))
				}
			}
		}
	}
}

Now lets have a look at the BreadCrumbTagLib which is defined in grails-app/taglib directory
BreadCrumbTagLib.groovy

package org.breadcrumbs

import org.breadcrumbs.BreadCrumbManager;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.LogFactory; 

class BreadCrumbTagLib {
	
	static def log = LogFactory.getLog("grails.app.breadCrumbTag")
	
	def breadCrumb = { attrs , body ->
	
			def manager = BreadCrumbManager.getInstance()
			def uri = request.getRequestURI() 
			def context = request.getContextPath() 
			
			def controller = params.controller
			def action = params.action
			
			def attrTitle =  attrs.title
			def attrLink = attrs.link
				
			// if controller and action are missing from params try to get them from request url
			if (!controller && !action && uri && context && uri.indexOf(context) != -1) {
				def uriParams = uri.substring(uri.indexOf(context) + (context.length() + 1), uri.length())
				def uriArray = uriParams.split('/')
				
				if (uriArray.size() >= 2 ) {
					controller = uriArray[0]
					action = uriArray[1]                       
				}
			}
			
			def crumbs = manager.getBreadCrumbs(controller, action)
			
			if (crumbs) {
				out << '<div class="breadcrumb"><ul>'
				def size = crumbs.size()
				crumbs.eachWithIndex { crumb, index ->
					out << '<li>'
					
					// override title and link of breadcrumb on current page (i.e. last bread crumb in hierarchy)
					// if name, link attributes are supplied
					if (index == size - 1) {
						if (attrTitle)
							crumb.title = attrTitle
						if (attrLink)
							crumb.link = attrLink
					}
					// set title to undefined if not found, associated 
					// renderer if present can overwrite it
					if (!crumb.title)
						crumb.title = "undefined"
					if (crumb.title && crumb.title.size() > 40)
						crumb.title = crumb.title.substring(0, 40) + "..."
						
					if (crumb.viewController && crumb.viewAction) {
						def content = g.include(controller:crumb.viewController, action:crumb.viewAction, breadcrumb:crumb, params:params)
						out << content
					} else if (crumb.viewTemplate) {
						def content = g.include(view:crumb.viewTemplate, breadcrumb:crumb, params: params)
						out << content
					} else if (crumb.linkToController && crumb.linkToAction && (size - 1 > index)){
						out << "<a href=\"${g.createLink (controller: crumb.linkToController, action: crumb.linkToAction)}\">${crumb.title}</a>"
					// if crumb has a link and its not the last vread crumb then show link else
					// just show the text
					} else if (crumb.link && (size - 1 > index)){
						out << "<a href=\"${crumb.link}\">${crumb.title}</a>"
					} else {
						out << "${crumb.title}"
					}
					out << "</li>"
					// do not print for last bread crumb
					if (size - 1 > index)
						out << "<li>&raquo;</li>"
				}
				out << "</ul></div>"
			}
	}
}

Lets also define basic css that is used for displaying breadcrumbs on pages

breadcrumbs.css


.breadcrumblist_selected {
	color:#a4db2f;
}

.breadcrumblist_selected a {
	text-decoration:none!important;
}

.breadcrumb {
	padding-left:15px; margin-top:5px; width:600px;
}
.breadcrumb ul {
	margin:0; padding:0;
}
.breadcrumb ul li {
	display:inline; font-size:14px; color:#FF9900
}
.breadcrumb ul li a {
	color:#2cbe0c
}

Now lets look at the demo code for breadcrumb implementation
Starting URL for the demo application will be something like http://localhost:8080/sample/samplePages/homeBreadCrumbPage if deployed on localhost on port 8080

Following is the controller code used for displaying sample pages in demo, each action defined in the controller refers to a demo page
samplePagesController.groovy

package com.pages

class samplePagesController {
	
	def homeBreadCrumbPage = { }

	def allItemsBreadCrumbPage = { }

	def level1BreadCrumbPage = { }
	
	def level2BreadCrumbPage = { }
	
	def level3BreadCrumbPage = { }
	
	def itemDetailsBreadCrumbPage = { }
	
	def simpleBreadCrumb = { }
	
	def simpleBreadCrumbWithAttr = { }
}

Lets look at the code for some of the views, code for remaining views can be found in zipped code attached with the post

Simple breadcrumb page includes the css that we defined for breadcrumbs and has a <g:breadCrumb/> tag at a place where breadcrumb needs to be displayed

simpleBreadCrumb.gsp

<html>
	<head>
		<link rel="stylesheet" href="${resource(dir:'css',file:'breadcrumbs.css')}" />
	</head>
  	<body>
	<g:breadCrumb/>
	<h3>Simple breadcrumbs</h3>
	</body>
</html>

The image below how breadcrumb will be shown in running application, The ‘Home’ link is clickable and will take user to page defined in configuration XML, please note that for the displayed page, controller is ‘samplePages’ and action is ‘simpleBreadCrumb’. If you look at first section of configuration XML you will figure out how breadcrumb hierarchy is being displayed

The next sample view that we are going to look at is itemDetailsBreadCrumbPage.gsp

itemDetailsBreadCrumbPage.gsp

<html>
	<head>
		<link rel="stylesheet" href="${resource(dir:'css',file:'breadcrumbs.css')}" />
	</head>
  	<body>
	<g:breadCrumb title='Selected Item'/>
	<h3>Item Details Page</h3>
	</body>
</html>

The image below shows how the breadcrumb will get rendered on runtime

Please note that the drop down displayed for breadcrumbs are clickable, we will shortly discuss how we displayed drop downs instead of text for these breadcrumbs but important point to note here is that you can have complex elements like sliders and popups here instead of drop downs as breadcrumb elements.

OK, lets see how these drop downs were displayed
We have following configuration in breadcrumbs.xml for this page

	<nav id="showItemCrumb" matchController="samplePages" matchAction="itemDetailsBreadCrumbPage"/>

The navigation hierarchy that we get for this page is homeCrumb->itemsLevel1Crumb->itemsLevel2Crumb->itemsLevel3Crumb->showItemCrumb

When we start rendering each breadcrumb in hierarchy and encounter ‘itemsLevel1Crumb’ we know that for this breadcrumb we need to execute controller ‘breadCrumb’ and action ‘renderLevel1Crumb’ to get the content so we invoke it

		<crumb id="itemsLevel1Crumb" viewController="breadCrumb" viewAction="renderLevel1Crumb" />
		<crumb id="itemsLevel2Crumb" viewController="breadCrumb" viewAction="renderLevel2Crumb" />
		<crumb id="itemsLevel3Crumb" viewController="breadCrumb" viewAction="renderLevel3Crumb" />

Following is the source code for controller breadCrumb
breadCrumbController.groovy

package org.breadcrumb

class breadCrumbController {

    def renderLevel1Crumb = { }
	
	def renderLevel2Crumb = { }
	
	def renderLevel3Crumb = { }
}

Following are the contents if renderLevel1Crumb.gsp
renderLevel1Crumb.gsp

<select>
<option>Level1 - A</option>
<option>Level1 - B</option>
<option>Level1 - C</option>
<option>Level1 - D</option>
</select>

Hence the breadcrumb with id ‘itemsLevel1Crumb’ gets rendered into a drop down, similar is the case with other drop down breadcrumbs.

You can download the code here. Note that application was created with grails 1.2.2

Use following URL for running the demo after deploying it on grails (please update host/port configuration)
http://localhost:8080/sample/samplePages/homeBreadCrumbPage

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s