Commits

Dimitris Zenios committed deaba36

Initial commit

Comments (0)

Files changed (24)

+.classpath
+.project
+.settings/
+target/
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<groupId>com.zenios</groupId>
+	<artifactId>tapestry-zbreadcrumbs</artifactId>
+	<version>0.0.1-SNAPSHOT</version>
+	
+  <dependencies>
+	<dependency>
+		<groupId>org.apache.tapestry</groupId>
+		<artifactId>tapestry-core</artifactId>
+		<version>${tapestry-release-version}</version>
+	</dependency>
+	
+  </dependencies>
+
+	<properties>
+		<tapestry-release-version>5.3.3</tapestry-release-version>
+	</properties>
+</project>

src/main/java/com/zenios/tapestry/breadcrumbs/annotation/BreadCrumb.java

+package com.zenios.tapestry.breadcrumbs.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface BreadCrumb {
+	boolean ignore() default false;
+	boolean reset() default false;
+	boolean titleReset() default false;
+}

src/main/java/com/zenios/tapestry/breadcrumbs/components/BreadCrumbInfo.java

+package com.zenios.tapestry.breadcrumbs.components;
+
+import org.apache.tapestry5.annotations.Parameter;
+import org.apache.tapestry5.ioc.annotations.Inject;
+import org.apache.tapestry5.services.ComponentSource;
+
+import com.zenios.tapestry.breadcrumbs.other.BreadCrumbObject;
+import com.zenios.tapestry.breadcrumbs.utils.BreadCrumbUtils;
+
+public class BreadCrumbInfo {
+    @Parameter(required = true, principal = true, autoconnect = true)
+    private BreadCrumbObject crumb;
+    
+    @Inject
+    private ComponentSource componentSource;
+        
+    public String getPage() {
+    	return crumb.getParams().getLogicalPageName();
+    }
+    
+    public String getTitle() {
+    	String title = crumb.getTitle();
+    	
+    	if(title == null) {
+    		title = BreadCrumbUtils.getTitle(componentSource.getPage(crumb.getParams().getLogicalPageName()), crumb.getParams());
+    	}
+    	return title;
+    	
+    }
+}

src/main/java/com/zenios/tapestry/breadcrumbs/components/BreadCrumbsLoop.java

+package com.zenios.tapestry.breadcrumbs.components;
+
+import java.util.List;
+
+import org.apache.tapestry5.annotations.Component;
+import org.apache.tapestry5.annotations.Parameter;
+import org.apache.tapestry5.annotations.Property;
+import org.apache.tapestry5.grid.GridConstants;
+import org.apache.tapestry5.internal.TapestryInternalUtils;
+import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
+
+import com.zenios.tapestry.breadcrumbs.other.BreadCrumbObject;
+import com.zenios.tapestry.breadcrumbs.other.BreadCrumbsModel;
+
+@SuppressWarnings("unused")
+public class BreadCrumbsLoop {
+
+    @Parameter(cache = false)
+    private String rowClass;
+    
+   	@Parameter
+    private int rowIndex;
+        
+    @Parameter(value = "componentResources.container")
+    private BreadCrumbsModel breadCrumbsModel;
+    
+    @Parameter(required = true)
+    @Property(write = false)
+    private BreadCrumbObject row;
+    	
+    @Component(parameters="crumb=row")
+    private BreadCrumbInfo breadCrumb;
+    
+    private int dataRowIndex;
+    
+    private int endRow;
+    
+    private int startRow;
+    
+
+    
+    
+    public String getRowClass()
+    {
+        List<String> classes = CollectionFactory.newList();
+
+        // Not a cached parameter, so careful to only access it once.
+
+        String rc = rowClass;
+
+        if (rc != null) classes.add(rc);
+
+        if (dataRowIndex == startRow) classes.add(GridConstants.FIRST_CLASS);
+
+        if (dataRowIndex == endRow) classes.add(GridConstants.LAST_CLASS);
+
+        return TapestryInternalUtils.toClassAttributeValue(classes);
+    }
+    
+    void setupForRow(int rowIndex)
+    {
+        row = breadCrumbsModel.getRow(rowIndex);
+    }
+    
+    boolean setupRender()
+    {
+    	int size = breadCrumbsModel.getSize();
+        startRow = 0;
+        endRow = size - 1;
+
+        dataRowIndex = startRow;
+        
+        return size != 0;
+    }
+    
+    boolean beginRender()
+    {
+        // Setup for this row.
+
+        setupForRow(dataRowIndex);
+
+        // Update the index parameter (which starts from zero).
+        rowIndex = dataRowIndex - startRow;
+
+
+        return row != null;
+    }
+
+    boolean afterRender()
+    {
+        dataRowIndex++;
+
+        // Abort the loop when we hit a null row, or when we've exhausted the range we need to
+        // display.
+
+        return row == null || dataRowIndex > endRow;
+    }
+
+}

src/main/java/com/zenios/tapestry/breadcrumbs/components/BreadCrumbsTrail.java

+package com.zenios.tapestry.breadcrumbs.components;
+
+import java.util.List;
+
+import org.apache.tapestry5.BindingConstants;
+import org.apache.tapestry5.Block;
+import org.apache.tapestry5.annotations.Component;
+import org.apache.tapestry5.annotations.Parameter;
+import org.apache.tapestry5.annotations.Property;
+import org.apache.tapestry5.annotations.SessionState;
+import org.apache.tapestry5.annotations.SupportsInformalParameters;
+import org.apache.tapestry5.corelib.components.Any;
+import org.apache.tapestry5.services.PageRenderRequestParameters;
+
+import com.zenios.tapestry.breadcrumbs.other.BreadCrumbObject;
+import com.zenios.tapestry.breadcrumbs.other.BreadCrumbsConstants;
+import com.zenios.tapestry.breadcrumbs.other.BreadCrumbsModel;
+import com.zenios.tapestry.breadcrumbs.session.BreadCrumbsList;
+
+@SuppressWarnings("unused")
+@SupportsInformalParameters
+public class BreadCrumbsTrail implements BreadCrumbsModel {
+	
+	@SessionState
+	private BreadCrumbsList breadCrumbs;
+	
+    @Parameter(name = "class", defaultPrefix = BindingConstants.LITERAL,value = BindingConstants.SYMBOL + ":" + BreadCrumbsConstants.BREADCRUMB_TRAIL_CSS_CLASS)
+    @Property(write = false)
+    private String trailClass;
+    
+    @Component(parameters = "class=trailClass", inheritInformalParameters = true)
+    @Property
+    private Any trail;
+    
+    @Component(parameters ="row=row" , publishParameters = "rowIndex,rowClass")
+    @Property
+    private BreadCrumbsLoop trailCrumbs;
+    
+    @Parameter(value = "block:empty",defaultPrefix = BindingConstants.LITERAL)
+    private Block empty;
+    
+    @Parameter(principal = true)
+    private BreadCrumbObject row;
+    
+
+    private CachingBreadCrumbSource cachingSource;
+    
+    Object setupRender()
+    {
+    	cachingSource = new CachingBreadCrumbSource(breadCrumbs.getBreadCrumbs());
+    	
+        return cachingSource.getAvailableRows() == 0 ? empty : null;
+    }
+    
+    static class CachingBreadCrumbSource
+    {
+        private final List<BreadCrumbObject> delegate;
+
+        private boolean availableRowsCached;
+
+        private int availableRows;
+
+        CachingBreadCrumbSource(List<BreadCrumbObject> delegate)
+        {
+            this.delegate = delegate;
+        }
+
+        public int getAvailableRows()
+        {
+            if (!availableRowsCached)
+            {
+                availableRows = delegate.size();
+                availableRowsCached = true;
+            }
+
+            return availableRows;
+        }
+
+        public BreadCrumbObject getRowValue(int index)
+        {
+            return delegate.get(index);
+        }
+    }
+
+	public int getSize() {
+		return cachingSource.getAvailableRows();
+	}
+
+	public BreadCrumbObject getRow(int rowIndex) {
+		return cachingSource.getRowValue(rowIndex);
+	}
+	
+    public BreadCrumbObject getRow()
+    {
+        return row;
+    }
+
+    public void setRow(BreadCrumbObject row)
+    {
+        this.row = row;
+    }
+
+}

src/main/java/com/zenios/tapestry/breadcrumbs/filter/BreadcrumbsPageRenderRequestFilter.java

+package com.zenios.tapestry.breadcrumbs.filter;
+
+import java.io.IOException;
+
+import org.apache.tapestry5.services.PageRenderRequestFilter;
+import org.apache.tapestry5.services.PageRenderRequestHandler;
+import org.apache.tapestry5.services.PageRenderRequestParameters;
+
+import com.zenios.tapestry.breadcrumbs.services.BreadCrumbsService;
+
+public class BreadcrumbsPageRenderRequestFilter implements PageRenderRequestFilter {
+	private final BreadCrumbsService breadCrumbsService;
+	
+	public BreadcrumbsPageRenderRequestFilter(BreadCrumbsService breadCrumbsService) {
+		this.breadCrumbsService = breadCrumbsService;
+	}
+
+	public void handle(PageRenderRequestParameters arg0, PageRenderRequestHandler arg1) throws IOException {
+		breadCrumbsService.addToBreadCrumbs(arg0);
+		arg1.handle(arg0);
+	}
+
+}

src/main/java/com/zenios/tapestry/breadcrumbs/other/BreadCrumbObject.java

+package com.zenios.tapestry.breadcrumbs.other;
+
+import org.apache.tapestry5.services.PageRenderRequestParameters;
+
+public class BreadCrumbObject {
+	private final PageRenderRequestParameters params;
+	private final String title;
+	
+	public BreadCrumbObject(PageRenderRequestParameters params,String title) {
+		this.params = params;
+		this.title = title;
+	}
+
+	public PageRenderRequestParameters getParams() {
+		return params;
+	}
+
+	public String getTitle() {
+		return title;
+	}
+
+    public boolean equals(BreadCrumbObject obj)
+    {
+        if (this == obj)
+            return true;
+
+        if (obj == null || getClass() != obj.getClass())
+            return false;
+
+        return obj.getParams().equals(this.params) && obj.getTitle().equals(this.title);
+    }
+}

src/main/java/com/zenios/tapestry/breadcrumbs/other/BreadCrumbsConstants.java

+package com.zenios.tapestry.breadcrumbs.other;
+
+public class BreadCrumbsConstants {
+	public static final String BREADCRUMBS_FILTER = "breadcrumbs";
+	public static final String MAX_CRUMBS_TO_SAVE = "breadcrumbs-to-save";
+	public static final String DISCARD_DUPLICATES = "breadcrumbs-discrd-duplicates";
+    public static final String BREADCRUMB_TRAIL_CSS_CLASS = "tapestry-zbreadcrumbs.css_class";
+}

src/main/java/com/zenios/tapestry/breadcrumbs/other/BreadCrumbsEvents.java

+package com.zenios.tapestry.breadcrumbs.other;
+
+public class BreadCrumbsEvents {
+	public static final String GET_TITLE = "zbreadcrumbs-get-title";
+}

src/main/java/com/zenios/tapestry/breadcrumbs/other/BreadCrumbsFilter.java

+package com.zenios.tapestry.breadcrumbs.other;
+
+public enum BreadCrumbsFilter {
+	RESET,
+	IGNORE,
+	TITLE_CHANGE
+}

src/main/java/com/zenios/tapestry/breadcrumbs/other/BreadCrumbsModel.java

+package com.zenios.tapestry.breadcrumbs.other;
+
+
+public interface BreadCrumbsModel {
+	int getSize();
+
+	BreadCrumbObject getRow(int rowIndex);
+}

src/main/java/com/zenios/tapestry/breadcrumbs/other/BreadCrumbsTrailConstants.java

+package com.zenios.tapestry.breadcrumbs.other;
+
+public class BreadCrumbsTrailConstants {
+    public static final String TRAIL_CLASS = "t-zbreadcrumbs";
+}

src/main/java/com/zenios/tapestry/breadcrumbs/services/BreadCrumbsModule.java

+package com.zenios.tapestry.breadcrumbs.services;
+
+import org.apache.tapestry5.ioc.Configuration;
+import org.apache.tapestry5.ioc.MappedConfiguration;
+import org.apache.tapestry5.ioc.OrderedConfiguration;
+import org.apache.tapestry5.ioc.ServiceBinder;
+import org.apache.tapestry5.ioc.annotations.Symbol;
+import org.apache.tapestry5.services.ApplicationStateContribution;
+import org.apache.tapestry5.services.ApplicationStateCreator;
+import org.apache.tapestry5.services.LibraryMapping;
+import org.apache.tapestry5.services.PageRenderRequestFilter;
+
+import com.zenios.tapestry.breadcrumbs.filter.BreadcrumbsPageRenderRequestFilter;
+import com.zenios.tapestry.breadcrumbs.other.BreadCrumbsConstants;
+import com.zenios.tapestry.breadcrumbs.other.BreadCrumbsTrailConstants;
+import com.zenios.tapestry.breadcrumbs.services.impl.BreadCrumbsServiceImpl;
+import com.zenios.tapestry.breadcrumbs.session.BreadCrumbsList;
+import com.zenios.tapestry.breadcrumbs.session.BreadCrumbsListImpl;
+
+public class BreadCrumbsModule {
+	
+	 public static void bind(final ServiceBinder binder) {
+		 binder.bind(BreadCrumbsService.class,BreadCrumbsServiceImpl.class);
+	 }
+	
+    public static void contributeFactoryDefaults(final MappedConfiguration<String, String> configuration) {
+        configuration.add(BreadCrumbsConstants.MAX_CRUMBS_TO_SAVE, "10");
+        configuration.add(BreadCrumbsConstants.DISCARD_DUPLICATES, "false");
+        configuration.add(BreadCrumbsConstants.BREADCRUMB_TRAIL_CSS_CLASS, BreadCrumbsTrailConstants.TRAIL_CLASS);
+    }
+    
+	public static void contributePageRenderRequestHandler(OrderedConfiguration<PageRenderRequestFilter> configuration) {
+		configuration.addInstance(BreadCrumbsConstants.BREADCRUMBS_FILTER, BreadcrumbsPageRenderRequestFilter.class);
+	}
+	
+	public static void contributeApplicationStateManager(MappedConfiguration<Class<?>, ApplicationStateContribution> configuration,@Symbol(BreadCrumbsConstants.MAX_CRUMBS_TO_SAVE) final int crumbsToSave,@Symbol(BreadCrumbsConstants.DISCARD_DUPLICATES) final boolean discardDuplicates) {
+		ApplicationStateCreator<BreadCrumbsList> breadCrumbsListCreator = new ApplicationStateCreator<BreadCrumbsList>()
+		{
+			public BreadCrumbsList create() {
+				return new BreadCrumbsListImpl(crumbsToSave,discardDuplicates);
+			}
+		};
+
+		configuration.add(BreadCrumbsList.class, new ApplicationStateContribution("session", breadCrumbsListCreator));
+	}
+	
+	
+    public static void contributeComponentClassResolver(final Configuration<LibraryMapping> configuration) {
+        configuration.add(new LibraryMapping("zcrumb", "com.zenios.tapestry.breadcrumbs"));
+    }
+}

src/main/java/com/zenios/tapestry/breadcrumbs/services/BreadCrumbsService.java

+package com.zenios.tapestry.breadcrumbs.services;
+
+import org.apache.tapestry5.ioc.annotations.UsesMappedConfiguration;
+import org.apache.tapestry5.services.PageRenderRequestParameters;
+
+import com.zenios.tapestry.breadcrumbs.other.BreadCrumbsFilter;
+
+@UsesMappedConfiguration(BreadCrumbsFilter.class)
+public interface BreadCrumbsService {
+	public void addToBreadCrumbs(PageRenderRequestParameters params);
+}

src/main/java/com/zenios/tapestry/breadcrumbs/services/impl/BreadCrumbsServiceImpl.java

+package com.zenios.tapestry.breadcrumbs.services.impl;
+
+import java.lang.annotation.Annotation;
+import java.util.Map;
+
+import org.apache.tapestry5.runtime.Component;
+import org.apache.tapestry5.services.ApplicationStateManager;
+import org.apache.tapestry5.services.ComponentSource;
+import org.apache.tapestry5.services.PageRenderRequestParameters;
+
+import com.zenios.tapestry.breadcrumbs.annotation.BreadCrumb;
+import com.zenios.tapestry.breadcrumbs.other.BreadCrumbObject;
+import com.zenios.tapestry.breadcrumbs.other.BreadCrumbsFilter;
+import com.zenios.tapestry.breadcrumbs.services.BreadCrumbsService;
+import com.zenios.tapestry.breadcrumbs.session.BreadCrumbsList;
+import com.zenios.tapestry.breadcrumbs.session.BreadCrumbsListImpl;
+import com.zenios.tapestry.breadcrumbs.utils.AntPathMatcher;
+import com.zenios.tapestry.breadcrumbs.utils.BreadCrumbUtils;
+import com.zenios.tapestry.breadcrumbs.utils.PatternMatcher;
+
+public class BreadCrumbsServiceImpl implements BreadCrumbsService {
+	
+	private final ApplicationStateManager applicationStateManager;
+	private final PatternMatcher patternMatcher;
+	private final Map<String,BreadCrumbsFilter> pathFilters;
+	private final ComponentSource componentSource;
+	
+	public BreadCrumbsServiceImpl(ApplicationStateManager applicationStateManager,Map<String,BreadCrumbsFilter> pathFilters,ComponentSource componentSource) {
+		this.applicationStateManager = applicationStateManager;
+		this.patternMatcher = createPathMatcher();
+		this.pathFilters = pathFilters;
+		this.componentSource = componentSource;
+	}
+
+	public void addToBreadCrumbs(PageRenderRequestParameters params) {
+		BreadCrumbsListImpl breadCrumbsList = (BreadCrumbsListImpl) applicationStateManager.get(BreadCrumbsList.class);
+		String pageName = params.getLogicalPageName();
+    	Component page = componentSource.getPage(pageName);
+    	boolean titleReset = false;
+    	
+    	for(String path:pathFilters.keySet()) {
+			if (patternMatcher.matches(path, pageName)) {	
+				BreadCrumbsFilter filter = pathFilters.get(path);
+				if(filter == BreadCrumbsFilter.IGNORE) {
+					return;
+				}
+				
+				if(filter == BreadCrumbsFilter.TITLE_CHANGE) {
+					titleReset = true;
+				}
+				
+				if(filter == BreadCrumbsFilter.RESET) {
+					breadCrumbsList.clear();
+					break;
+				}
+			}
+		}
+
+    	BreadCrumb breadCrumb = findAnnotation(page.getClass(),BreadCrumb.class);
+    	if(breadCrumb != null) {
+	    	if(breadCrumb.reset()) {
+				breadCrumbsList.clear();
+	    	}
+	    	
+	    	if(breadCrumb.ignore()) {
+	    		return;
+	    	}
+    	}
+    	
+ 
+    	String title = null;
+    	if(breadCrumb == null || !breadCrumb.titleReset() || !titleReset) {
+    		title = BreadCrumbUtils.getTitle(page, params);
+    	}
+    
+		breadCrumbsList.add(new BreadCrumbObject(params,title));
+	}
+	
+	
+	private PatternMatcher createPathMatcher() {
+		return new AntPathMatcher() {
+			public boolean matches(String pattern, String source) {
+		    	return super.matches(pattern, source.toLowerCase());
+		    }
+		};
+	}
+	
+    private <T extends Annotation> T findAnnotation(final Class<?> clazz, final Class<T> annotation) {
+        T result = clazz.getAnnotation(annotation);
+
+        if (result == null && clazz.getSuperclass() != null) {
+            result = this.findAnnotation(clazz.getSuperclass(), annotation);
+        }
+
+        return result;
+    }
+}

src/main/java/com/zenios/tapestry/breadcrumbs/session/BreadCrumbsList.java

+package com.zenios.tapestry.breadcrumbs.session;
+
+import java.io.Serializable;
+import java.util.List;
+
+import com.zenios.tapestry.breadcrumbs.other.BreadCrumbObject;
+
+public interface BreadCrumbsList extends Serializable {
+	public List<BreadCrumbObject> getBreadCrumbs();
+}

src/main/java/com/zenios/tapestry/breadcrumbs/session/BreadCrumbsListImpl.java

+package com.zenios.tapestry.breadcrumbs.session;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.zenios.tapestry.breadcrumbs.other.BreadCrumbObject;
+
+public class BreadCrumbsListImpl implements BreadCrumbsList {
+	private static final long serialVersionUID = -8457209111821626779L;
+	private final List<BreadCrumbObject> items;
+	private final Object mutex;
+	private final int size;
+	private final boolean discardDuplicates;
+
+	public BreadCrumbsListImpl(int size,boolean discardDuplicates) {
+		this.items = new ArrayList<BreadCrumbObject>(size);
+		this.mutex = new Object();
+		this.size = size;
+		this.discardDuplicates = discardDuplicates;
+	}
+
+	public void clear() {
+		synchronized(mutex) {
+			items.clear();
+		}
+	}
+
+	public void add(BreadCrumbObject breadCrumb) {
+		synchronized(mutex) {
+			final int index = items.indexOf(breadCrumb);
+			if (discardDuplicates && index != -1) {
+				items.subList(index + 1, items.size()).clear();
+			} else {
+				while (items.size() == size) {
+					items.remove(0);
+				}
+				items.add(breadCrumb);
+			}
+		}
+	}
+
+	public List<BreadCrumbObject> getBreadCrumbs() {
+		synchronized(mutex) {
+			return new ArrayList<BreadCrumbObject>(items);
+		}
+	}
+}

src/main/java/com/zenios/tapestry/breadcrumbs/utils/AntPathMatcher.java

+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package com.zenios.tapestry.breadcrumbs.utils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * <p>PathMatcher implementation for Ant-style path patterns.
+ * Examples are provided below.</p>
+ *
+ * <p>Part of this mapping code has been kindly borrowed from
+ * <a href="http://ant.apache.org">Apache Ant</a>.
+ *
+ * <p>The mapping matches URLs using the following rules:<br>
+ * <ul>
+ * <li>? matches one character</li>
+ * <li>* matches zero or more characters</li>
+ * <li>** matches zero or more 'directories' in a path</li>
+ * </ul>
+ *
+ * <p>Some examples:<br>
+ * <ul>
+ * <li><code>com/t?st.jsp</code> - matches <code>com/test.jsp</code> but also
+ * <code>com/tast.jsp</code> or <code>com/txst.jsp</code></li>
+ * <li><code>com/*.jsp</code> - matches all <code>.jsp</code> files in the
+ * <code>com</code> directory</li>
+ * <li><code>com/&#42;&#42;/test.jsp</code> - matches all <code>test.jsp</code>
+ * files underneath the <code>com</code> path</li>
+ * <li><code>org/apache/shiro/&#42;&#42;/*.jsp</code> - matches all <code>.jsp</code>
+ * files underneath the <code>org/apache/shiro</code> path</li>
+ * <li><code>org/&#42;&#42;/servlet/bla.jsp</code> - matches
+ * <code>org/apache/shiro/servlet/bla.jsp</code> but also
+ * <code>org/apache/shiro/testing/servlet/bla.jsp</code> and
+ * <code>org/servlet/bla.jsp</code></li>
+ * </ul>
+ *
+ * <p><b>N.B.</b>: This class was borrowed (with much appreciation) from the
+ * <a href="http://www.springframework.org">Spring Framework</a> with modifications.  We didn't want to reinvent the
+ * wheel of great work they've done, but also didn't want to force every Shiro user to depend on Spring</p>
+ *
+ * <p>As per the Apache 2.0 license, the original copyright notice and all author and copyright information have
+ * remained in tact.</p>
+ *
+ * @borrowed: org.apache.shiro.util.AntPathMatcher
+ */
+public class AntPathMatcher implements PatternMatcher {
+
+    //TODO - complete JavaDoc
+
+    /**
+     * Default path separator: "/"
+     */
+    public static final String DEFAULT_PATH_SEPARATOR = "/";
+
+    private String pathSeparator = DEFAULT_PATH_SEPARATOR;
+
+
+    /**
+     * Set the path separator to use for pattern parsing.
+     * Default is "/", as in Ant.
+     */
+    public void setPathSeparator(String pathSeparator) {
+        this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR);
+    }
+
+
+    public boolean isPattern(String path) {
+        return (path.indexOf('*') != -1 || path.indexOf('?') != -1);
+    }
+
+    public boolean matches(String pattern, String source) {
+        return match(pattern, source);
+    }
+
+    public boolean match(String pattern, String path) {
+        return doMatch(pattern, path, true);
+    }
+
+    public boolean matchStart(String pattern, String path) {
+        return doMatch(pattern, path, false);
+    }
+
+
+    /**
+     * Actually match the given <code>path</code> against the given <code>pattern</code>.
+     *
+     * @param pattern   the pattern to match against
+     * @param path      the path String to test
+     * @param fullMatch whether a full pattern match is required
+     *                  (else a pattern match as far as the given base path goes is sufficient)
+     * @return <code>true</code> if the supplied <code>path</code> matched,
+     *         <code>false</code> if it didn't
+     */
+    protected boolean doMatch(String pattern, String path, boolean fullMatch) {
+        if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {
+            return false;
+        }
+
+        String[] pattDirs = tokenizeToStringArray(pattern, this.pathSeparator,true,true);
+        String[] pathDirs = tokenizeToStringArray(path, this.pathSeparator,true,true);
+
+        int pattIdxStart = 0;
+        int pattIdxEnd = pattDirs.length - 1;
+        int pathIdxStart = 0;
+        int pathIdxEnd = pathDirs.length - 1;
+
+        // Match all elements up to the first **
+        while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
+            String patDir = pattDirs[pattIdxStart];
+            if ("**".equals(patDir)) {
+                break;
+            }
+            if (!matchStrings(patDir, pathDirs[pathIdxStart])) {
+                return false;
+            }
+            pattIdxStart++;
+            pathIdxStart++;
+        }
+
+        if (pathIdxStart > pathIdxEnd) {
+            // Path is exhausted, only match if rest of pattern is * or **'s
+            if (pattIdxStart > pattIdxEnd) {
+                return (pattern.endsWith(this.pathSeparator) ?
+                        path.endsWith(this.pathSeparator) : !path.endsWith(this.pathSeparator));
+            }
+            if (!fullMatch) {
+                return true;
+            }
+            if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") &&
+                    path.endsWith(this.pathSeparator)) {
+                return true;
+            }
+            for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
+                if (!pattDirs[i].equals("**")) {
+                    return false;
+                }
+            }
+            return true;
+        } else if (pattIdxStart > pattIdxEnd) {
+            // String not exhausted, but pattern is. Failure.
+            return false;
+        } else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
+            // Path start definitely matches due to "**" part in pattern.
+            return true;
+        }
+
+        // up to last '**'
+        while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
+            String patDir = pattDirs[pattIdxEnd];
+            if (patDir.equals("**")) {
+                break;
+            }
+            if (!matchStrings(patDir, pathDirs[pathIdxEnd])) {
+                return false;
+            }
+            pattIdxEnd--;
+            pathIdxEnd--;
+        }
+        if (pathIdxStart > pathIdxEnd) {
+            // String is exhausted
+            for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
+                if (!pattDirs[i].equals("**")) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {
+            int patIdxTmp = -1;
+            for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {
+                if (pattDirs[i].equals("**")) {
+                    patIdxTmp = i;
+                    break;
+                }
+            }
+            if (patIdxTmp == pattIdxStart + 1) {
+                // '**/**' situation, so skip one
+                pattIdxStart++;
+                continue;
+            }
+            // Find the pattern between padIdxStart & padIdxTmp in str between
+            // strIdxStart & strIdxEnd
+            int patLength = (patIdxTmp - pattIdxStart - 1);
+            int strLength = (pathIdxEnd - pathIdxStart + 1);
+            int foundIdx = -1;
+
+            strLoop:
+            for (int i = 0; i <= strLength - patLength; i++) {
+                for (int j = 0; j < patLength; j++) {
+                    String subPat = (String) pattDirs[pattIdxStart + j + 1];
+                    String subStr = (String) pathDirs[pathIdxStart + i + j];
+                    if (!matchStrings(subPat, subStr)) {
+                        continue strLoop;
+                    }
+                }
+                foundIdx = pathIdxStart + i;
+                break;
+            }
+
+            if (foundIdx == -1) {
+                return false;
+            }
+
+            pattIdxStart = patIdxTmp;
+            pathIdxStart = foundIdx + patLength;
+        }
+
+        for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
+            if (!pattDirs[i].equals("**")) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Tests whether or not a string matches against a pattern.
+     * The pattern may contain two special characters:<br>
+     * '*' means zero or more characters<br>
+     * '?' means one and only one character
+     *
+     * @param pattern pattern to match against.
+     *                Must not be <code>null</code>.
+     * @param str     string which must be matched against the pattern.
+     *                Must not be <code>null</code>.
+     * @return <code>true</code> if the string matches against the
+     *         pattern, or <code>false</code> otherwise.
+     */
+    private boolean matchStrings(String pattern, String str) {
+        char[] patArr = pattern.toCharArray();
+        char[] strArr = str.toCharArray();
+        int patIdxStart = 0;
+        int patIdxEnd = patArr.length - 1;
+        int strIdxStart = 0;
+        int strIdxEnd = strArr.length - 1;
+        char ch;
+
+        boolean containsStar = false;
+        for (char aPatArr : patArr) {
+            if (aPatArr == '*') {
+                containsStar = true;
+                break;
+            }
+        }
+
+        if (!containsStar) {
+            // No '*'s, so we make a shortcut
+            if (patIdxEnd != strIdxEnd) {
+                return false; // Pattern and string do not have the same size
+            }
+            for (int i = 0; i <= patIdxEnd; i++) {
+                ch = patArr[i];
+                if (ch != '?') {
+                    if (ch != strArr[i]) {
+                        return false;// Character mismatch
+                    }
+                }
+            }
+            return true; // String matches against pattern
+        }
+
+
+        if (patIdxEnd == 0) {
+            return true; // Pattern contains only '*', which matches anything
+        }
+
+        // Process characters before first star
+        while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd) {
+            if (ch != '?') {
+                if (ch != strArr[strIdxStart]) {
+                    return false;// Character mismatch
+                }
+            }
+            patIdxStart++;
+            strIdxStart++;
+        }
+        if (strIdxStart > strIdxEnd) {
+            // All characters in the string are used. Check if only '*'s are
+            // left in the pattern. If so, we succeeded. Otherwise failure.
+            for (int i = patIdxStart; i <= patIdxEnd; i++) {
+                if (patArr[i] != '*') {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        // Process characters after last star
+        while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd) {
+            if (ch != '?') {
+                if (ch != strArr[strIdxEnd]) {
+                    return false;// Character mismatch
+                }
+            }
+            patIdxEnd--;
+            strIdxEnd--;
+        }
+        if (strIdxStart > strIdxEnd) {
+            // All characters in the string are used. Check if only '*'s are
+            // left in the pattern. If so, we succeeded. Otherwise failure.
+            for (int i = patIdxStart; i <= patIdxEnd; i++) {
+                if (patArr[i] != '*') {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        // process pattern between stars. padIdxStart and patIdxEnd point
+        // always to a '*'.
+        while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {
+            int patIdxTmp = -1;
+            for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
+                if (patArr[i] == '*') {
+                    patIdxTmp = i;
+                    break;
+                }
+            }
+            if (patIdxTmp == patIdxStart + 1) {
+                // Two stars next to each other, skip the first one.
+                patIdxStart++;
+                continue;
+            }
+            // Find the pattern between padIdxStart & padIdxTmp in str between
+            // strIdxStart & strIdxEnd
+            int patLength = (patIdxTmp - patIdxStart - 1);
+            int strLength = (strIdxEnd - strIdxStart + 1);
+            int foundIdx = -1;
+            strLoop:
+            for (int i = 0; i <= strLength - patLength; i++) {
+                for (int j = 0; j < patLength; j++) {
+                    ch = patArr[patIdxStart + j + 1];
+                    if (ch != '?') {
+                        if (ch != strArr[strIdxStart + i + j]) {
+                            continue strLoop;
+                        }
+                    }
+                }
+
+                foundIdx = strIdxStart + i;
+                break;
+            }
+
+            if (foundIdx == -1) {
+                return false;
+            }
+
+            patIdxStart = patIdxTmp;
+            strIdxStart = foundIdx + patLength;
+        }
+
+        // All characters in the string are used. Check if only '*'s are left
+        // in the pattern. If so, we succeeded. Otherwise failure.
+        for (int i = patIdxStart; i <= patIdxEnd; i++) {
+            if (patArr[i] != '*') {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Given a pattern and a full path, determine the pattern-mapped part.
+     * <p>For example:
+     * <ul>
+     * <li>'<code>/docs/cvs/commit.html</code>' and '<code>/docs/cvs/commit.html</code> -> ''</li>
+     * <li>'<code>/docs/*</code>' and '<code>/docs/cvs/commit</code> -> '<code>cvs/commit</code>'</li>
+     * <li>'<code>/docs/cvs/*.html</code>' and '<code>/docs/cvs/commit.html</code> -> '<code>commit.html</code>'</li>
+     * <li>'<code>/docs/**</code>' and '<code>/docs/cvs/commit</code> -> '<code>cvs/commit</code>'</li>
+     * <li>'<code>/docs/**\/*.html</code>' and '<code>/docs/cvs/commit.html</code> -> '<code>cvs/commit.html</code>'</li>
+     * <li>'<code>/*.html</code>' and '<code>/docs/cvs/commit.html</code> -> '<code>docs/cvs/commit.html</code>'</li>
+     * <li>'<code>*.html</code>' and '<code>/docs/cvs/commit.html</code> -> '<code>/docs/cvs/commit.html</code>'</li>
+     * <li>'<code>*</code>' and '<code>/docs/cvs/commit.html</code> -> '<code>/docs/cvs/commit.html</code>'</li>
+     * </ul>
+     * <p>Assumes that {@link #match} returns <code>true</code> for '<code>pattern</code>'
+     * and '<code>path</code>', but does <strong>not</strong> enforce this.
+     */
+    public String extractPathWithinPattern(String pattern, String path) {
+        String[] patternParts = tokenizeToStringArray(pattern, this.pathSeparator,true,true);
+        String[] pathParts = tokenizeToStringArray(path, this.pathSeparator,true,true);
+
+        StringBuilder buffer = new StringBuilder();
+
+        // Add any path parts that have a wildcarded pattern part.
+        int puts = 0;
+        for (int i = 0; i < patternParts.length; i++) {
+            String patternPart = patternParts[i];
+            if ((patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) && pathParts.length >= i + 1) {
+                if (puts > 0 || (i == 0 && !pattern.startsWith(this.pathSeparator))) {
+                    buffer.append(this.pathSeparator);
+                }
+                buffer.append(pathParts[i]);
+                puts++;
+            }
+        }
+
+        // Append any trailing path parts.
+        for (int i = patternParts.length; i < pathParts.length; i++) {
+            if (puts > 0 || i > 0) {
+                buffer.append(this.pathSeparator);
+            }
+            buffer.append(pathParts[i]);
+        }
+
+        return buffer.toString();
+    }
+    
+    private String[] tokenizeToStringArray(String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) {
+
+        if (str == null) {
+            return null;
+        }
+        StringTokenizer st = new StringTokenizer(str, delimiters);
+        List<String> tokens = new ArrayList<String>();
+        while (st.hasMoreTokens()) {
+            String token = st.nextToken();
+            if (trimTokens) {
+                token = token.trim();
+            }
+            if (!ignoreEmptyTokens || token.length() > 0) {
+                tokens.add(token);
+            }
+        }
+        return toStringArray(tokens);
+    }
+    
+    private String[] toStringArray(Collection<String> collection) {
+        if (collection == null) {
+            return null;
+        }
+        return (String[]) collection.toArray(new String[collection.size()]);
+    }
+
+}

src/main/java/com/zenios/tapestry/breadcrumbs/utils/BreadCrumbUtils.java

+package com.zenios.tapestry.breadcrumbs.utils;
+
+import org.apache.tapestry5.ComponentEventCallback;
+import org.apache.tapestry5.internal.util.Holder;
+import org.apache.tapestry5.ioc.Messages;
+import org.apache.tapestry5.runtime.Component;
+import org.apache.tapestry5.services.PageRenderRequestParameters;
+
+import com.zenios.tapestry.breadcrumbs.other.BreadCrumbsEvents;
+
+public class BreadCrumbUtils {
+	public static String getTitle(Component page,PageRenderRequestParameters params) {
+		String pageName = params.getLogicalPageName();
+		String title = null;
+		final Holder<String> valueHolder = Holder.create();
+		ComponentEventCallback<String> callback = new ComponentEventCallback<String>()
+				{
+			public boolean handleResult(String result)
+			{
+				valueHolder.put(result);
+
+				return true;
+			}
+				};
+
+
+			page.getComponentResources().triggerContextEvent(BreadCrumbsEvents.GET_TITLE, params.getActivationContext(), callback);
+			title = valueHolder.get();
+			if(title == null) {
+				String key = new StringBuilder(pageName).append("-crumb").toString();
+				Messages messages = page.getComponentResources().getMessages();
+				if(messages.contains(key)) {
+					title = messages.get(key);
+				}
+			}
+			title = title != null ? title : pageName;
+			return title;
+	}
+}

src/main/java/com/zenios/tapestry/breadcrumbs/utils/PatternMatcher.java

+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package com.zenios.tapestry.breadcrumbs.utils;
+
+/**
+ * Interface for components that can match source strings against a specified pattern string.
+ * <p/>
+ * Different implementations can support different pattern types, for example, Ant style path expressions, or
+ * regular expressions, or other types of text based patterns.
+ *
+ * borrowed: org.apache.shiro.util.PatternMatche
+ */
+public interface PatternMatcher {
+
+    /**
+     * Returns <code>true</code> if the given <code>source</code> matches the specified <code>pattern</code>,
+     * <code>false</code> otherwise.
+     *
+     * @param pattern the pattern to match against
+     * @param source  the source to match
+     * @return <code>true</code> if the given <code>source</code> matches the specified <code>pattern</code>,
+     *         <code>false</code> otherwise.
+     */
+    boolean matches(String pattern, String source);
+}

src/main/resources/com/zenios/tapestry/breadcrumbs/components/BreadCrumbInfo.tml

+<li xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd">
+	${title}
+</li>

src/main/resources/com/zenios/tapestry/breadcrumbs/components/BreadCrumbsLoop.tml

+<li rowClass="${rowClass}" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd">
+	<div t:id="breadCrumb"/>
+</li>

src/main/resources/com/zenios/tapestry/breadcrumbs/components/BreadCrumbsTrail.tml

+<div class="t-zbreadcrumbs" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd">
+
+    <ul t:id="trail">
+        <tbody>
+        	<li t:id="trailCrumbs"/>
+        </tbody>
+    </ul>
+
+    <t:block id="empty">${message:no-grid-data-to-display}</t:block>
+
+</div>