From 7a401a3ff909bf82fb4068d6dba430497f74084a Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Mon, 05 May 2014 11:32:46 -0400
Subject: [PATCH] Allow plugins to extend the top navbar and repository navbar

---
 src/main/java/com/gitblit/wicket/pages/RepositoryPage.java           |   61 ++-
 src/main/java/com/gitblit/wicket/UrlExternalFormComparator.java      |   39 ++
 src/main/java/com/gitblit/wicket/PluginClassResolver.java            |  122 +++++++
 src/main/java/com/gitblit/wicket/pages/ProjectsPage.java             |   10 
 src/main/java/com/gitblit/extensions/NavLinkExtension.java           |   10 
 src/main/java/com/gitblit/wicket/GitBlitWebApp.java                  |  106 +++++
 src/main/java/com/gitblit/wicket/pages/RepositoriesPage.java         |   10 
 src/main/java/com/gitblit/wicket/pages/UserPage.java                 |   10 
 src/main/java/com/gitblit/wicket/panels/DropDownMenu.java            |   45 ++
 src/main/java/com/gitblit/wicket/GitblitWicketApp.java               |   72 ++++
 src/main/java/com/gitblit/wicket/pages/RootPage.java                 |   57 +--
 /dev/null                                                            |   99 -----
 src/site/plugins_extensions.mkd                                      |   58 +++
 src/main/java/com/gitblit/wicket/pages/ActivityPage.java             |   10 
 src/main/java/com/gitblit/extensions/RepositoryNavLinkExtension.java |   12 
 src/main/java/com/gitblit/wicket/pages/DashboardPage.java            |   10 
 src/main/java/com/gitblit/wicket/panels/NavigationPanel.java         |   49 +-
 src/main/java/com/gitblit/extensions/GitblitWicketPlugin.java        |   49 ++
 src/main/java/com/gitblit/wicket/pages/ProjectPage.java              |   14 
 src/main/java/com/gitblit/models/NavLink.java                        |  140 ++++++++
 20 files changed, 749 insertions(+), 234 deletions(-)

diff --git a/src/main/java/com/gitblit/extensions/GitblitWicketPlugin.java b/src/main/java/com/gitblit/extensions/GitblitWicketPlugin.java
new file mode 100644
index 0000000..130f499
--- /dev/null
+++ b/src/main/java/com/gitblit/extensions/GitblitWicketPlugin.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2014 gitblit.com.
+ *
+ * Licensed 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.gitblit.extensions;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.IInitializer;
+
+import ro.fortsoft.pf4j.PluginWrapper;
+
+import com.gitblit.wicket.GitblitWicketApp;
+
+/**
+ * A Gitblit plugin that is allowed to extend the Wicket webapp.
+ *
+ * @author James Moger
+ * @since 1.6.0
+ */
+public abstract class GitblitWicketPlugin extends GitblitPlugin implements IInitializer  {
+
+	public GitblitWicketPlugin(PluginWrapper wrapper) {
+		super(wrapper);
+	}
+
+	@Override
+	public final void init(Application application) {
+		init((GitblitWicketApp) application);
+	}
+
+	/**
+	 * Allows plugins to extend the web application.
+	 *
+	 * @param app
+	 * @since 1.6.0
+	 */
+	protected abstract void init(GitblitWicketApp app);
+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/extensions/AdminMenuExtension.java b/src/main/java/com/gitblit/extensions/NavLinkExtension.java
similarity index 75%
rename from src/main/java/com/gitblit/extensions/AdminMenuExtension.java
rename to src/main/java/com/gitblit/extensions/NavLinkExtension.java
index 8fe4288..c895860 100644
--- a/src/main/java/com/gitblit/extensions/AdminMenuExtension.java
+++ b/src/main/java/com/gitblit/extensions/NavLinkExtension.java
@@ -19,22 +19,22 @@
 
 import ro.fortsoft.pf4j.ExtensionPoint;
 
-import com.gitblit.models.Menu.MenuItem;
+import com.gitblit.models.NavLink;
 import com.gitblit.models.UserModel;
 
 /**
- * Extension point to contribute administration menu items.
+ * Extension point to contribute top-level navigation links.
  *
  * @author James Moger
  * @since 1.6.0
  *
  */
-public abstract class AdminMenuExtension implements ExtensionPoint {
+public abstract class NavLinkExtension implements ExtensionPoint {
 
 	/**
 	 * @param user
 	 * @since 1.6.0
-	 * @return a list of menu items
+	 * @return a list of nav links
 	 */
-	public abstract List<MenuItem> getMenuItems(UserModel user);
+	public abstract List<NavLink> getNavLinks(UserModel user);
 }
diff --git a/src/main/java/com/gitblit/extensions/AdminMenuExtension.java b/src/main/java/com/gitblit/extensions/RepositoryNavLinkExtension.java
similarity index 69%
copy from src/main/java/com/gitblit/extensions/AdminMenuExtension.java
copy to src/main/java/com/gitblit/extensions/RepositoryNavLinkExtension.java
index 8fe4288..2b05c5a 100644
--- a/src/main/java/com/gitblit/extensions/AdminMenuExtension.java
+++ b/src/main/java/com/gitblit/extensions/RepositoryNavLinkExtension.java
@@ -19,22 +19,24 @@
 
 import ro.fortsoft.pf4j.ExtensionPoint;
 
-import com.gitblit.models.Menu.MenuItem;
+import com.gitblit.models.NavLink;
+import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.UserModel;
 
 /**
- * Extension point to contribute administration menu items.
+ * Extension point to contribute repository page navigation links.
  *
  * @author James Moger
  * @since 1.6.0
  *
  */
-public abstract class AdminMenuExtension implements ExtensionPoint {
+public abstract class RepositoryNavLinkExtension implements ExtensionPoint {
 
 	/**
 	 * @param user
+	 * @param repository
 	 * @since 1.6.0
-	 * @return a list of menu items
+	 * @return a list of nav links
 	 */
-	public abstract List<MenuItem> getMenuItems(UserModel user);
+	public abstract List<NavLink> getNavLinks(UserModel user, RepositoryModel repository);
 }
diff --git a/src/main/java/com/gitblit/models/NavLink.java b/src/main/java/com/gitblit/models/NavLink.java
new file mode 100644
index 0000000..993d695
--- /dev/null
+++ b/src/main/java/com/gitblit/models/NavLink.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed 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.gitblit.models;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.WebPage;
+
+import com.gitblit.models.Menu.MenuItem;
+
+/**
+ * Represents a navigation link for the navigation panel.
+ *
+ * @author James Moger
+ *
+ */
+public abstract class NavLink implements Serializable {
+	private static final long serialVersionUID = 1L;
+
+	public final String translationKey;
+	public final boolean hiddenPhone;
+
+	public NavLink(String translationKey, boolean hiddenPhone) {
+		this.translationKey = translationKey;
+		this.hiddenPhone = hiddenPhone;
+	}
+
+
+	/**
+	 * Represents a Wicket page link.
+	 *
+	 * @author James Moger
+	 *
+	 */
+	public static class PageNavLink extends NavLink implements Serializable {
+		private static final long serialVersionUID = 1L;
+
+		public final Class<? extends WebPage> pageClass;
+		public final PageParameters params;
+
+		public PageNavLink(String translationKey, Class<? extends WebPage> pageClass) {
+			this(translationKey, pageClass, null);
+		}
+
+		public PageNavLink(String translationKey, Class<? extends WebPage> pageClass,
+				PageParameters params) {
+			this(translationKey, pageClass, params, false);
+		}
+
+		public PageNavLink(String translationKey, Class<? extends WebPage> pageClass,
+				PageParameters params, boolean hiddenPhone) {
+			super(translationKey, hiddenPhone);
+			this.pageClass = pageClass;
+			this.params = params;
+		}
+	}
+
+	/**
+	 * Represents an explicitly href link.
+	 *
+	 * @author James Moger
+	 *
+	 */
+	public static class ExternalNavLink extends NavLink implements Serializable {
+
+		private static final long serialVersionUID = 1L;
+
+		public final String url;
+
+		public ExternalNavLink(String keyOrText, String url) {
+			super(keyOrText, false);
+			this.url = url;
+		}
+
+		public ExternalNavLink(String keyOrText, String url, boolean hiddenPhone) {
+			super(keyOrText,  hiddenPhone);
+			this.url = url;
+		}
+	}
+
+	/**
+	 * Represents a DropDownMenu for the current page.
+	 *
+	 * @author James Moger
+	 *
+	 */
+	public static class DropDownPageMenuNavLink extends PageNavLink implements Serializable {
+
+		private static final long serialVersionUID = 1L;
+
+		public final List<MenuItem> menuItems;
+
+		public DropDownPageMenuNavLink(String keyOrText, Class<? extends WebPage> pageClass) {
+			this(keyOrText, pageClass, false);
+		}
+
+		public DropDownPageMenuNavLink(String keyOrText, Class<? extends WebPage> pageClass, boolean hiddenPhone) {
+			super(keyOrText, pageClass, null, hiddenPhone);
+			menuItems = new ArrayList<MenuItem>();
+		}
+	}
+
+	/**
+	 * Represents a DropDownMenu.
+	 *
+	 * @author James Moger
+	 *
+	 */
+	public static class DropDownMenuNavLink extends NavLink implements Serializable {
+
+		private static final long serialVersionUID = 1L;
+
+		public final List<MenuItem> menuItems;
+
+		public DropDownMenuNavLink(String keyOrText) {
+			this(keyOrText, false);
+		}
+
+		public DropDownMenuNavLink(String keyOrText, boolean hiddenPhone) {
+			super(keyOrText, hiddenPhone);
+			menuItems = new ArrayList<MenuItem>();
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java
index 3ca7d48..d3aa62f 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java
@@ -28,8 +28,12 @@
 import org.apache.wicket.markup.html.WebPage;
 import org.apache.wicket.protocol.http.WebApplication;
 
+import ro.fortsoft.pf4j.PluginState;
+import ro.fortsoft.pf4j.PluginWrapper;
+
 import com.gitblit.IStoredSettings;
 import com.gitblit.Keys;
+import com.gitblit.extensions.GitblitWicketPlugin;
 import com.gitblit.manager.IAuthenticationManager;
 import com.gitblit.manager.IFederationManager;
 import com.gitblit.manager.IGitblit;
@@ -83,7 +87,7 @@
 import com.gitblit.wicket.pages.UserPage;
 import com.gitblit.wicket.pages.UsersPage;
 
-public class GitBlitWebApp extends WebApplication {
+public class GitBlitWebApp extends WebApplication implements GitblitWicketApp {
 
 	private final Class<? extends WebPage> homePageClass = MyDashboardPage.class;
 
@@ -210,11 +214,29 @@
 		mount("/forks", ForksPage.class, "r");
 		mount("/fork", ForkPage.class, "r");
 
+		// allow started Wicket plugins to initialize
+		for (PluginWrapper pluginWrapper : pluginManager.getPlugins()) {
+			if (PluginState.STARTED != pluginWrapper.getPluginState()) {
+				continue;
+			}
+			if (pluginWrapper.getPlugin() instanceof GitblitWicketPlugin) {
+				GitblitWicketPlugin wicketPlugin = (GitblitWicketPlugin) pluginWrapper.getPlugin();
+				wicketPlugin.init(this);
+			}
+		}
+
+		 // customize the Wicket class resolver to load from plugins
+        PluginClassResolver classResolver = new PluginClassResolver(pluginManager);
+        getApplicationSettings().setClassResolver(classResolver);
+
 		getMarkupSettings().setDefaultMarkupEncoding("UTF-8");
-		super.init();
 	}
 
-	private void mount(String location, Class<? extends WebPage> clazz, String... parameters) {
+	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#mount(java.lang.String, java.lang.Class, java.lang.String)
+	 */
+	@Override
+	public void mount(String location, Class<? extends WebPage> clazz, String... parameters) {
 		if (parameters == null) {
 			parameters = new String[] {};
 		}
@@ -230,15 +252,26 @@
 		}
 	}
 
+	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#getHomePage()
+	 */
 	@Override
 	public Class<? extends WebPage> getHomePage() {
 		return homePageClass;
 	}
 
+	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#isCacheablePage(java.lang.String)
+	 */
+	@Override
 	public boolean isCacheablePage(String mountPoint) {
 		return cacheablePages.containsKey(mountPoint);
 	}
 
+	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#getCacheControl(java.lang.String)
+	 */
+	@Override
 	public CacheControl getCacheControl(String mountPoint) {
 		return cacheablePages.get(mountPoint);
 	}
@@ -254,15 +287,18 @@
 		return gitBlitWebSession;
 	}
 
+	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#settings()
+	 */
+	@Override
 	public IStoredSettings settings() {
 		return settings;
 	}
 
-	/**
-	 * Is Gitblit running in debug mode?
-	 *
-	 * @return true if Gitblit is running in debug mode
+	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#isDebugMode()
 	 */
+	@Override
 	public boolean isDebugMode() {
 		return runtimeManager.isDebugMode();
 	}
@@ -271,58 +307,114 @@
 	 * These methods look strange... and they are... but they are the first
 	 * step towards modularization across multiple commits.
 	 */
+	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#getBootDate()
+	 */
+	@Override
 	public Date getBootDate() {
 		return runtimeManager.getBootDate();
 	}
 
+	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#getLastActivityDate()
+	 */
+	@Override
 	public Date getLastActivityDate() {
 		return repositoryManager.getLastActivityDate();
 	}
 
+	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#runtime()
+	 */
+	@Override
 	public IRuntimeManager runtime() {
 		return runtimeManager;
 	}
 
+	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#plugins()
+	 */
+	@Override
 	public IPluginManager plugins() {
 		return pluginManager;
 	}
 
+	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#notifier()
+	 */
+	@Override
 	public INotificationManager notifier() {
 		return notificationManager;
 	}
 
+	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#users()
+	 */
+	@Override
 	public IUserManager users() {
 		return userManager;
 	}
 
+	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#authentication()
+	 */
+	@Override
 	public IAuthenticationManager authentication() {
 		return authenticationManager;
 	}
 
+	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#keys()
+	 */
+	@Override
 	public IPublicKeyManager keys() {
 		return publicKeyManager;
 	}
 
+	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#repositories()
+	 */
+	@Override
 	public IRepositoryManager repositories() {
 		return repositoryManager;
 	}
 
+	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#projects()
+	 */
+	@Override
 	public IProjectManager projects() {
 		return projectManager;
 	}
 
+	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#federation()
+	 */
+	@Override
 	public IFederationManager federation() {
 		return federationManager;
 	}
 
+	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#gitblit()
+	 */
+	@Override
 	public IGitblit gitblit() {
 		return gitblit;
 	}
 
+	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#tickets()
+	 */
+	@Override
 	public ITicketService tickets() {
 		return gitblit.getTicketService();
 	}
 
+	/* (non-Javadoc)
+	 * @see com.gitblit.wicket.Webapp#getTimezone()
+	 */
+	@Override
 	public TimeZone getTimezone() {
 		return runtimeManager.getTimezone();
 	}
diff --git a/src/main/java/com/gitblit/wicket/GitblitWicketApp.java b/src/main/java/com/gitblit/wicket/GitblitWicketApp.java
new file mode 100644
index 0000000..a56e699
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/GitblitWicketApp.java
@@ -0,0 +1,72 @@
+package com.gitblit.wicket;
+
+import java.util.Date;
+import java.util.TimeZone;
+
+import org.apache.wicket.markup.html.WebPage;
+
+import com.gitblit.IStoredSettings;
+import com.gitblit.manager.IAuthenticationManager;
+import com.gitblit.manager.IFederationManager;
+import com.gitblit.manager.IGitblit;
+import com.gitblit.manager.INotificationManager;
+import com.gitblit.manager.IPluginManager;
+import com.gitblit.manager.IProjectManager;
+import com.gitblit.manager.IRepositoryManager;
+import com.gitblit.manager.IRuntimeManager;
+import com.gitblit.manager.IUserManager;
+import com.gitblit.tickets.ITicketService;
+import com.gitblit.transport.ssh.IPublicKeyManager;
+
+public interface GitblitWicketApp {
+
+	public abstract void mount(String location, Class<? extends WebPage> clazz, String... parameters);
+
+	public abstract Class<? extends WebPage> getHomePage();
+
+	public abstract boolean isCacheablePage(String mountPoint);
+
+	public abstract CacheControl getCacheControl(String mountPoint);
+
+	public abstract IStoredSettings settings();
+
+	/**
+	 * Is Gitblit running in debug mode?
+	 *
+	 * @return true if Gitblit is running in debug mode
+	 */
+	public abstract boolean isDebugMode();
+
+	/*
+	 * These methods look strange... and they are... but they are the first
+	 * step towards modularization across multiple commits.
+	 */
+	public abstract Date getBootDate();
+
+	public abstract Date getLastActivityDate();
+
+	public abstract IRuntimeManager runtime();
+
+	public abstract IPluginManager plugins();
+
+	public abstract INotificationManager notifier();
+
+	public abstract IUserManager users();
+
+	public abstract IAuthenticationManager authentication();
+
+	public abstract IPublicKeyManager keys();
+
+	public abstract IRepositoryManager repositories();
+
+	public abstract IProjectManager projects();
+
+	public abstract IFederationManager federation();
+
+	public abstract IGitblit gitblit();
+
+	public abstract ITicketService tickets();
+
+	public abstract TimeZone getTimezone();
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/PageRegistration.java b/src/main/java/com/gitblit/wicket/PageRegistration.java
deleted file mode 100644
index 9fd8f87..0000000
--- a/src/main/java/com/gitblit/wicket/PageRegistration.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright 2011 gitblit.com.
- *
- * Licensed 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.gitblit.wicket;
-
-import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.List;
-
-import org.apache.wicket.PageParameters;
-import org.apache.wicket.markup.html.WebPage;
-
-import com.gitblit.models.Menu.MenuItem;
-
-/**
- * Represents a page link registration for the topbar.
- *
- * @author James Moger
- *
- */
-public class PageRegistration implements Serializable {
-	private static final long serialVersionUID = 1L;
-
-	public final String translationKey;
-	public final Class<? extends WebPage> pageClass;
-	public final PageParameters params;
-	public final boolean hiddenPhone;
-
-	public PageRegistration(String translationKey, Class<? extends WebPage> pageClass) {
-		this(translationKey, pageClass, null);
-	}
-
-	public PageRegistration(String translationKey, Class<? extends WebPage> pageClass,
-			PageParameters params) {
-		this(translationKey, pageClass, params, false);
-	}
-
-	public PageRegistration(String translationKey, Class<? extends WebPage> pageClass,
-			PageParameters params, boolean hiddenPhone) {
-		this.translationKey = translationKey;
-		this.pageClass = pageClass;
-		this.params = params;
-		this.hiddenPhone = hiddenPhone;
-	}
-
-	/**
-	 * Represents a page link to a non-Wicket page. Might be external.
-	 *
-	 * @author James Moger
-	 *
-	 */
-	public static class OtherPageLink extends PageRegistration {
-
-		private static final long serialVersionUID = 1L;
-
-		public final String url;
-
-		public OtherPageLink(String keyOrText, String url) {
-			super(keyOrText, null);
-			this.url = url;
-		}
-
-		public OtherPageLink(String keyOrText, String url, boolean hiddenPhone) {
-			super(keyOrText, null, null, hiddenPhone);
-			this.url = url;
-		}
-	}
-
-	/**
-	 * Represents a DropDownMenu for the topbar
-	 *
-	 * @author James Moger
-	 *
-	 */
-	public static class DropDownMenuRegistration extends PageRegistration {
-
-		private static final long serialVersionUID = 1L;
-
-		public final List<MenuItem> menuItems;
-
-		public DropDownMenuRegistration(String keyOrText, Class<? extends WebPage> pageClass) {
-			super(keyOrText, pageClass);
-			menuItems = new ArrayList<MenuItem>();
-		}
-	}
-
-}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/PluginClassResolver.java b/src/main/java/com/gitblit/wicket/PluginClassResolver.java
new file mode 100644
index 0000000..ba53b04
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/PluginClassResolver.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2014 gitblit.com.
+ *
+ * Licensed 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.gitblit.wicket;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.WicketRuntimeException;
+import org.apache.wicket.application.IClassResolver;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ro.fortsoft.pf4j.PluginState;
+import ro.fortsoft.pf4j.PluginWrapper;
+
+import com.gitblit.manager.IPluginManager;
+
+/**
+ * Resolves plugin classes and resources.
+ */
+public class PluginClassResolver implements IClassResolver {
+	private static final Logger logger = LoggerFactory.getLogger(PluginClassResolver.class);
+
+	private final IPluginManager pluginManager;
+
+	public PluginClassResolver(IPluginManager pluginManager) {
+		this.pluginManager = pluginManager;
+	}
+
+	@Override
+	public Class<?> resolveClass(final String className) throws ClassNotFoundException {
+		boolean debugEnabled = logger.isDebugEnabled();
+
+		for (PluginWrapper plugin : pluginManager.getPlugins()) {
+			if (PluginState.STARTED != plugin.getPluginState()) {
+				// ignore this plugin
+				continue;
+			}
+
+			try {
+				return plugin.getPluginClassLoader().loadClass(className);
+			} catch (ClassNotFoundException cnfx) {
+				if (debugEnabled) {
+					logger.debug("ClassResolver '{}' cannot find class: '{}'", plugin.getPluginId(), className);
+				}
+			}
+		}
+
+		throw new ClassNotFoundException(className);
+	}
+
+	@Override
+	public Iterator<URL> getResources(final String name) {
+		Set<URL> urls = new TreeSet<URL>(new UrlExternalFormComparator());
+
+		for (PluginWrapper plugin : pluginManager.getPlugins()) {
+			if (PluginState.STARTED != plugin.getPluginState()) {
+				// ignore this plugin
+				continue;
+			}
+
+			Iterator<URL> it = getResources(name, plugin);
+			while (it.hasNext()) {
+				URL url = it.next();
+				urls.add(url);
+			}
+		}
+
+		return urls.iterator();
+	}
+
+	protected Iterator<URL> getResources(String name, PluginWrapper plugin) {
+		HashSet<URL> loadedFiles = new HashSet<URL>();
+		try {
+			// Try the classloader for the wicket jar/bundle
+			Enumeration<URL> resources = plugin.getPluginClassLoader().getResources(name);
+			loadResources(resources, loadedFiles);
+
+			// Try the classloader for the user's application jar/bundle
+			resources = Application.get().getClass().getClassLoader().getResources(name);
+			loadResources(resources, loadedFiles);
+
+			// Try the context class loader
+			resources = Thread.currentThread().getContextClassLoader().getResources(name);
+			loadResources(resources, loadedFiles);
+		} catch (IOException e) {
+			throw new WicketRuntimeException(e);
+		}
+
+		return loadedFiles.iterator();
+	}
+
+	private void loadResources(Enumeration<URL> resources, Set<URL> loadedFiles) {
+		if (resources != null) {
+			while (resources.hasMoreElements()) {
+				final URL url = resources.nextElement();
+				if (!loadedFiles.contains(url)) {
+					loadedFiles.add(url);
+				}
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/UrlExternalFormComparator.java b/src/main/java/com/gitblit/wicket/UrlExternalFormComparator.java
new file mode 100644
index 0000000..90f4b32
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/UrlExternalFormComparator.java
@@ -0,0 +1,39 @@
+/*
+ * 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.gitblit.wicket;
+
+import java.net.URL;
+import java.util.Comparator;
+
+/**
+ * A comparator of URL instances.
+ *
+ * Comparing URLs with their implementation of #equals() is
+ * bad because it may cause problems like DNS resolving, or other
+ * slow checks. This comparator uses the external form of an URL
+ * to make a simple comparison of two Strings.
+ *
+ * @since 1.5.6
+ */
+public class UrlExternalFormComparator implements Comparator<URL>
+{
+	@Override
+	public int compare(URL url1, URL url2)
+	{
+		return url1.toExternalForm().compareTo(url2.toExternalForm());
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/ActivityPage.java b/src/main/java/com/gitblit/wicket/pages/ActivityPage.java
index 0870ff9..c505a66 100644
--- a/src/main/java/com/gitblit/wicket/pages/ActivityPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/ActivityPage.java
@@ -32,14 +32,14 @@
 import com.gitblit.Keys;
 import com.gitblit.models.Activity;
 import com.gitblit.models.Menu.ParameterMenuItem;
+import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
 import com.gitblit.models.Metric;
+import com.gitblit.models.NavLink;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.utils.ActivityUtils;
 import com.gitblit.utils.StringUtils;
 import com.gitblit.wicket.CacheControl;
 import com.gitblit.wicket.CacheControl.LastModified;
-import com.gitblit.wicket.PageRegistration;
-import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.charting.Chart;
 import com.gitblit.wicket.charting.Charts;
@@ -135,8 +135,8 @@
 	}
 
 	@Override
-	protected void addDropDownMenus(List<PageRegistration> pages) {
-		DropDownMenuRegistration filters = new DropDownMenuRegistration("gb.filters",
+	protected void addDropDownMenus(List<NavLink> navLinks) {
+		DropDownPageMenuNavLink filters = new DropDownPageMenuNavLink("gb.filters",
 				ActivityPage.class);
 
 		PageParameters currentParameters = getPageParameters();
@@ -155,7 +155,7 @@
 			// Reset Filter
 			filters.menuItems.add(new ParameterMenuItem(getString("gb.reset")));
 		}
-		pages.add(filters);
+		navLinks.add(filters);
 	}
 
 	/**
diff --git a/src/main/java/com/gitblit/wicket/pages/DashboardPage.java b/src/main/java/com/gitblit/wicket/pages/DashboardPage.java
index 16b0b73..9c10e01 100644
--- a/src/main/java/com/gitblit/wicket/pages/DashboardPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/DashboardPage.java
@@ -37,7 +37,9 @@
 import com.gitblit.Keys;
 import com.gitblit.models.DailyLogEntry;
 import com.gitblit.models.Menu.ParameterMenuItem;
+import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
 import com.gitblit.models.Metric;
+import com.gitblit.models.NavLink;
 import com.gitblit.models.RefLogEntry;
 import com.gitblit.models.RepositoryCommit;
 import com.gitblit.models.RepositoryModel;
@@ -46,8 +48,6 @@
 import com.gitblit.utils.RefLogUtils;
 import com.gitblit.utils.StringUtils;
 import com.gitblit.wicket.GitBlitWebApp;
-import com.gitblit.wicket.PageRegistration;
-import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
 import com.gitblit.wicket.charting.Chart;
 import com.gitblit.wicket.charting.Charts;
 import com.gitblit.wicket.charting.Flotr2Charts;
@@ -141,10 +141,10 @@
 	}
 
 	@Override
-	protected void addDropDownMenus(List<PageRegistration> pages) {
+	protected void addDropDownMenus(List<NavLink> navLinks) {
 		PageParameters params = getPageParameters();
 
-		DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
+		DropDownPageMenuNavLink menu = new DropDownPageMenuNavLink("gb.filters",
 				GitBlitWebApp.get().getHomePage());
 
 		// preserve repository filter option on time choices
@@ -155,7 +155,7 @@
 			menu.menuItems.add(new ParameterMenuItem(getString("gb.reset")));
 		}
 
-		pages.add(menu);
+		navLinks.add(menu);
 	}
 
 
diff --git a/src/main/java/com/gitblit/wicket/pages/ProjectPage.java b/src/main/java/com/gitblit/wicket/pages/ProjectPage.java
index 6c8aa4f..d358b77 100644
--- a/src/main/java/com/gitblit/wicket/pages/ProjectPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/ProjectPage.java
@@ -29,6 +29,8 @@
 import com.gitblit.models.Menu.MenuDivider;
 import com.gitblit.models.Menu.MenuItem;
 import com.gitblit.models.Menu.ParameterMenuItem;
+import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
+import com.gitblit.models.NavLink;
 import com.gitblit.models.ProjectModel;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.UserModel;
@@ -40,8 +42,6 @@
 import com.gitblit.wicket.GitBlitWebApp;
 import com.gitblit.wicket.GitBlitWebSession;
 import com.gitblit.wicket.GitblitRedirectException;
-import com.gitblit.wicket.PageRegistration;
-import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.panels.FilterableRepositoryList;
 
@@ -161,10 +161,10 @@
 	}
 
 	@Override
-	protected void addDropDownMenus(List<PageRegistration> pages) {
+	protected void addDropDownMenus(List<NavLink> navLinks) {
 		PageParameters params = getPageParameters();
 
-		DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
+		DropDownPageMenuNavLink menu = new DropDownPageMenuNavLink("gb.filters",
 				ProjectPage.class);
 		// preserve time filter option on repository choices
 		menu.menuItems.addAll(getRepositoryFilterItems(params));
@@ -177,12 +177,12 @@
 			menu.menuItems.add(new ParameterMenuItem(getString("gb.reset"), "p", WicketUtils.getProjectName(params)));
 		}
 
-		pages.add(menu);
+		navLinks.add(menu);
 
-		DropDownMenuRegistration projects = new DropDownMenuRegistration("gb.projects",
+		DropDownPageMenuNavLink projects = new DropDownPageMenuNavLink("gb.projects",
 				ProjectPage.class);
 		projects.menuItems.addAll(getProjectsMenu());
-		pages.add(projects);
+		navLinks.add(projects);
 	}
 
 	@Override
diff --git a/src/main/java/com/gitblit/wicket/pages/ProjectsPage.java b/src/main/java/com/gitblit/wicket/pages/ProjectsPage.java
index c404ae6..f04fa78 100644
--- a/src/main/java/com/gitblit/wicket/pages/ProjectsPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/ProjectsPage.java
@@ -25,10 +25,10 @@
 
 import com.gitblit.Keys;
 import com.gitblit.models.Menu.ParameterMenuItem;
+import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
+import com.gitblit.models.NavLink;
 import com.gitblit.models.ProjectModel;
 import com.gitblit.wicket.GitBlitWebSession;
-import com.gitblit.wicket.PageRegistration;
-import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.panels.LinkPanel;
 
@@ -115,10 +115,10 @@
 	}
 
 	@Override
-	protected void addDropDownMenus(List<PageRegistration> pages) {
+	protected void addDropDownMenus(List<NavLink> navLinks) {
 		PageParameters params = getPageParameters();
 
-		DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
+		DropDownPageMenuNavLink menu = new DropDownPageMenuNavLink("gb.filters",
 				ProjectsPage.class);
 		// preserve time filter option on repository choices
 		menu.menuItems.addAll(getRepositoryFilterItems(params));
@@ -131,6 +131,6 @@
 			menu.menuItems.add(new ParameterMenuItem(getString("gb.reset")));
 		}
 
-		pages.add(menu);
+		navLinks.add(menu);
 	}
 }
diff --git a/src/main/java/com/gitblit/wicket/pages/RepositoriesPage.java b/src/main/java/com/gitblit/wicket/pages/RepositoriesPage.java
index 41fe057..a0b15a8 100644
--- a/src/main/java/com/gitblit/wicket/pages/RepositoriesPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/RepositoriesPage.java
@@ -30,14 +30,14 @@
 
 import com.gitblit.Keys;
 import com.gitblit.models.Menu.ParameterMenuItem;
+import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
+import com.gitblit.models.NavLink;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.utils.MarkdownUtils;
 import com.gitblit.utils.StringUtils;
 import com.gitblit.wicket.CacheControl;
 import com.gitblit.wicket.CacheControl.LastModified;
 import com.gitblit.wicket.GitBlitWebSession;
-import com.gitblit.wicket.PageRegistration;
-import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.panels.RepositoriesPanel;
 
@@ -92,10 +92,10 @@
 	}
 
 	@Override
-	protected void addDropDownMenus(List<PageRegistration> pages) {
+	protected void addDropDownMenus(List<NavLink> navLinks) {
 		PageParameters params = getPageParameters();
 
-		DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
+		DropDownPageMenuNavLink menu = new DropDownPageMenuNavLink("gb.filters",
 				RepositoriesPage.class);
 		// preserve time filter option on repository choices
 		menu.menuItems.addAll(getRepositoryFilterItems(params));
@@ -108,7 +108,7 @@
 			menu.menuItems.add(new ParameterMenuItem(getString("gb.reset")));
 		}
 
-		pages.add(menu);
+		navLinks.add(menu);
 	}
 
 	private String readMarkdown(String messageSource, String resource) {
diff --git a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
index 5ea99fd..165feed 100644
--- a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
@@ -21,7 +21,6 @@
 import java.util.Arrays;
 import java.util.Date;
 import java.util.HashMap;
-import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -49,6 +48,10 @@
 import com.gitblit.Constants;
 import com.gitblit.GitBlitException;
 import com.gitblit.Keys;
+import com.gitblit.extensions.RepositoryNavLinkExtension;
+import com.gitblit.models.NavLink;
+import com.gitblit.models.NavLink.ExternalNavLink;
+import com.gitblit.models.NavLink.PageNavLink;
 import com.gitblit.models.ProjectModel;
 import com.gitblit.models.RefModel;
 import com.gitblit.models.RepositoryModel;
@@ -66,8 +69,6 @@
 import com.gitblit.utils.StringUtils;
 import com.gitblit.wicket.CacheControl;
 import com.gitblit.wicket.GitBlitWebSession;
-import com.gitblit.wicket.PageRegistration;
-import com.gitblit.wicket.PageRegistration.OtherPageLink;
 import com.gitblit.wicket.SessionlessForm;
 import com.gitblit.wicket.TicketsUI;
 import com.gitblit.wicket.WicketUtils;
@@ -91,7 +92,6 @@
 
 	private Map<String, SubmoduleModel> submodules;
 
-	private final Map<String, PageRegistration> registeredPages;
 	private boolean showAdmin;
 	private boolean isOwner;
 
@@ -150,12 +150,11 @@
 			}
 		}
 
-		// register the available page links for this page and user
-		registeredPages = registerPages();
+		// register the available navigation links for this page and user
+		List<NavLink> navLinks = registerNavLinks();
 
-		// standard page links
-		List<PageRegistration> pages = new ArrayList<PageRegistration>(registeredPages.values());
-		NavigationPanel navigationPanel = new NavigationPanel("repositoryNavPanel", getRepoNavPageClass(), pages);
+		// standard navigation links
+		NavigationPanel navigationPanel = new NavigationPanel("repositoryNavPanel", getRepoNavPageClass(), navLinks);
 		add(navigationPanel);
 
 		add(new ExternalLink("syndication", SyndicationServlet.asLink(getRequest()
@@ -183,45 +182,56 @@
 		return new BugtraqProcessor(app().settings());
 	}
 
-	private Map<String, PageRegistration> registerPages() {
+	private List<NavLink> registerNavLinks() {
 		PageParameters params = null;
 		if (!StringUtils.isEmpty(repositoryName)) {
 			params = WicketUtils.newRepositoryParameter(repositoryName);
 		}
-		Map<String, PageRegistration> pages = new LinkedHashMap<String, PageRegistration>();
+		List<NavLink> navLinks = new ArrayList<NavLink>();
 
 		Repository r = getRepository();
 		RepositoryModel model = getRepositoryModel();
 
 		// standard links
 		if (RefLogUtils.getRefLogBranch(r) == null) {
-			pages.put("summary", new PageRegistration("gb.summary", SummaryPage.class, params));
+			navLinks.add(new PageNavLink("gb.summary", SummaryPage.class, params));
 		} else {
-			pages.put("summary", new PageRegistration("gb.summary", SummaryPage.class, params));
+			navLinks.add(new PageNavLink("gb.summary", SummaryPage.class, params));
 //			pages.put("overview", new PageRegistration("gb.overview", OverviewPage.class, params));
-			pages.put("reflog", new PageRegistration("gb.reflog", ReflogPage.class, params));
+			navLinks.add(new PageNavLink("gb.reflog", ReflogPage.class, params));
 		}
-		pages.put("commits", new PageRegistration("gb.commits", LogPage.class, params));
-		pages.put("tree", new PageRegistration("gb.tree", TreePage.class, params));
+		navLinks.add(new PageNavLink("gb.commits", LogPage.class, params));
+		navLinks.add(new PageNavLink("gb.tree", TreePage.class, params));
 		if (app().tickets().isReady() && (app().tickets().isAcceptingNewTickets(getRepositoryModel()) || app().tickets().hasTickets(getRepositoryModel()))) {
 			PageParameters tParams = new PageParameters(params);
 			for (String state : TicketsUI.openStatii) {
 				tParams.add(Lucene.status.name(), state);
 			}
-			pages.put("tickets", new PageRegistration("gb.tickets", TicketsPage.class, tParams));
+			navLinks.add(new PageNavLink("gb.tickets", TicketsPage.class, tParams));
 		}
-		pages.put("docs", new PageRegistration("gb.docs", DocsPage.class, params, true));
+		navLinks.add(new PageNavLink("gb.docs", DocsPage.class, params, true));
 		if (app().settings().getBoolean(Keys.web.allowForking, true)) {
-			pages.put("forks", new PageRegistration("gb.forks", ForksPage.class, params, true));
+			navLinks.add(new PageNavLink("gb.forks", ForksPage.class, params, true));
 		}
-		pages.put("compare", new PageRegistration("gb.compare", ComparePage.class, params, true));
+		navLinks.add(new PageNavLink("gb.compare", ComparePage.class, params, true));
 
 		// conditional links
-		// per-repository extra page links
+		// per-repository extra navlinks
 		if (JGitUtils.getPagesBranch(r) != null) {
-			OtherPageLink pagesLink = new OtherPageLink("gb.pages", PagesServlet.asLink(
+			ExternalNavLink pagesLink = new ExternalNavLink("gb.pages", PagesServlet.asLink(
 					getRequest().getRelativePathPrefixToContextRoot(), repositoryName, null), true);
-			pages.put("pages", pagesLink);
+			navLinks.add(pagesLink);
+		}
+
+		UserModel user = UserModel.ANONYMOUS;
+		if (GitBlitWebSession.get().isLoggedIn()) {
+			user = GitBlitWebSession.get().getUser();
+		}
+
+		// add repository nav link extensions
+		List<RepositoryNavLinkExtension> extensions = app().plugins().getExtensions(RepositoryNavLinkExtension.class);
+		for (RepositoryNavLinkExtension ext : extensions) {
+			navLinks.addAll(ext.getNavLinks(user, model));
 		}
 
 		// Conditionally add edit link
@@ -233,9 +243,8 @@
 			showAdmin = app().settings().getBoolean(Keys.web.allowAdministration, false);
 		}
 		isOwner = GitBlitWebSession.get().isLoggedIn()
-				&& (model.isOwner(GitBlitWebSession.get()
-						.getUsername()));
-		return pages;
+				&& (model.isOwner(GitBlitWebSession.get().getUsername()));
+		return navLinks;
 	}
 
 	protected boolean allowForkControls() {
diff --git a/src/main/java/com/gitblit/wicket/pages/RootPage.java b/src/main/java/com/gitblit/wicket/pages/RootPage.java
index 3003c70..a2f3a49 100644
--- a/src/main/java/com/gitblit/wicket/pages/RootPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/RootPage.java
@@ -50,6 +50,7 @@
 
 import com.gitblit.Constants;
 import com.gitblit.Keys;
+import com.gitblit.extensions.NavLinkExtension;
 import com.gitblit.extensions.UserMenuExtension;
 import com.gitblit.models.Menu.ExternalLinkMenuItem;
 import com.gitblit.models.Menu.MenuDivider;
@@ -57,13 +58,14 @@
 import com.gitblit.models.Menu.PageLinkMenuItem;
 import com.gitblit.models.Menu.ParameterMenuItem;
 import com.gitblit.models.Menu.ToggleMenuItem;
+import com.gitblit.models.NavLink;
+import com.gitblit.models.NavLink.PageNavLink;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.TeamModel;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.ModelUtils;
 import com.gitblit.utils.StringUtils;
 import com.gitblit.wicket.GitBlitWebSession;
-import com.gitblit.wicket.PageRegistration;
 import com.gitblit.wicket.SessionlessForm;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.panels.GravatarImage;
@@ -174,50 +176,37 @@
 		}
 
 		// navigation links
-		List<PageRegistration> pages = new ArrayList<PageRegistration>();
+		List<NavLink> navLinks = new ArrayList<NavLink>();
 		if (!authenticateView || (authenticateView && isLoggedIn)) {
-			pages.add(new PageRegistration(isLoggedIn ? "gb.myDashboard" : "gb.dashboard", MyDashboardPage.class,
+			navLinks.add(new PageNavLink(isLoggedIn ? "gb.myDashboard" : "gb.dashboard", MyDashboardPage.class,
 					getRootPageParameters()));
 			if (isLoggedIn && app().tickets().isReady()) {
-				pages.add(new PageRegistration("gb.myTickets", MyTicketsPage.class));
+				navLinks.add(new PageNavLink("gb.myTickets", MyTicketsPage.class));
 			}
-			pages.add(new PageRegistration("gb.repositories", RepositoriesPage.class,
+			navLinks.add(new PageNavLink("gb.repositories", RepositoriesPage.class,
 					getRootPageParameters()));
-			pages.add(new PageRegistration("gb.activity", ActivityPage.class, getRootPageParameters()));
+			navLinks.add(new PageNavLink("gb.activity", ActivityPage.class, getRootPageParameters()));
 			if (allowLucene) {
-				pages.add(new PageRegistration("gb.search", LuceneSearchPage.class));
-			}
-
-			UserModel user = GitBlitWebSession.get().getUser();
-
-			if (showAdmin) {
-				// admin dropdown menu
-				DropDownMenuRegistration adminMenu = new DropDownMenuRegistration("gb.adminMenuItem", MyDashboardPage.class);
-
-				adminMenu.menuItems.add(new PageLinkMenuItem(getString("gb.users"), UsersPage.class));
-
-				boolean showRegistrations = app().federation().canFederate()
-						&& app().settings().getBoolean(Keys.web.showFederationRegistrations, false);
-				if (showRegistrations) {
-					adminMenu.menuItems.add(new PageLinkMenuItem(getString("gb.federation"), FederationPage.class));
-				}
-
-				// allow plugins to contribute admin menu items
-				List<AdminMenuExtension> extensions = app().plugins().getExtensions(AdminMenuExtension.class);
-				for (AdminMenuExtension ext : extensions) {
-					adminMenu.menuItems.add(new MenuDivider());
-					adminMenu.menuItems.addAll(ext.getMenuItems(user));
-				}
-
-				pages.add(adminMenu);
+				navLinks.add(new PageNavLink("gb.search", LuceneSearchPage.class));
 			}
 
 			if (!authenticateView || (authenticateView && isLoggedIn)) {
-				addDropDownMenus(pages);
+				addDropDownMenus(navLinks);
+			}
+
+			UserModel user = UserModel.ANONYMOUS;
+			if (isLoggedIn) {
+				user = GitBlitWebSession.get().getUser();
+			}
+
+			// add nav link extensions
+			List<NavLinkExtension> extensions = app().plugins().getExtensions(NavLinkExtension.class);
+			for (NavLinkExtension ext : extensions) {
+				navLinks.addAll(ext.getNavLinks(user));
 			}
 		}
 
-		NavigationPanel navPanel = new NavigationPanel("navPanel", getRootNavPageClass(), pages);
+		NavigationPanel navPanel = new NavigationPanel("navPanel", getRootNavPageClass(), navLinks);
 		add(navPanel);
 
 		// display an error message cached from a redirect
@@ -309,7 +298,7 @@
 		return repositoryModels;
 	}
 
-	protected void addDropDownMenus(List<PageRegistration> pages) {
+	protected void addDropDownMenus(List<NavLink> navLinks) {
 
 	}
 
diff --git a/src/main/java/com/gitblit/wicket/pages/UserPage.java b/src/main/java/com/gitblit/wicket/pages/UserPage.java
index 0767621..6cb791e 100644
--- a/src/main/java/com/gitblit/wicket/pages/UserPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/UserPage.java
@@ -30,6 +30,8 @@
 
 import com.gitblit.Keys;
 import com.gitblit.models.Menu.ParameterMenuItem;
+import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
+import com.gitblit.models.NavLink;
 import com.gitblit.models.ProjectModel;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.UserModel;
@@ -37,8 +39,6 @@
 import com.gitblit.wicket.GitBlitWebApp;
 import com.gitblit.wicket.GitBlitWebSession;
 import com.gitblit.wicket.GitblitRedirectException;
-import com.gitblit.wicket.PageRegistration;
-import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.panels.GravatarImage;
 import com.gitblit.wicket.panels.LinkPanel;
@@ -127,10 +127,10 @@
 	}
 
 	@Override
-	protected void addDropDownMenus(List<PageRegistration> pages) {
+	protected void addDropDownMenus(List<NavLink> navLinks) {
 		PageParameters params = getPageParameters();
 
-		DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
+		DropDownPageMenuNavLink menu = new DropDownPageMenuNavLink("gb.filters",
 				UserPage.class);
 		// preserve time filter option on repository choices
 		menu.menuItems.addAll(getRepositoryFilterItems(params));
@@ -143,6 +143,6 @@
 			menu.menuItems.add(new ParameterMenuItem(getString("gb.reset")));
 		}
 
-		pages.add(menu);
+		navLinks.add(menu);
 	}
 }
diff --git a/src/main/java/com/gitblit/wicket/panels/DropDownMenu.java b/src/main/java/com/gitblit/wicket/panels/DropDownMenu.java
index f561143..4e7ae54 100644
--- a/src/main/java/com/gitblit/wicket/panels/DropDownMenu.java
+++ b/src/main/java/com/gitblit/wicket/panels/DropDownMenu.java
@@ -21,24 +21,24 @@
 import org.apache.wicket.markup.repeater.data.DataView;
 import org.apache.wicket.markup.repeater.data.ListDataProvider;
 
-import com.gitblit.models.Menu.MenuDivider;
 import com.gitblit.models.Menu.ExternalLinkMenuItem;
+import com.gitblit.models.Menu.MenuDivider;
 import com.gitblit.models.Menu.MenuItem;
 import com.gitblit.models.Menu.PageLinkMenuItem;
 import com.gitblit.models.Menu.ParameterMenuItem;
-import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
+import com.gitblit.models.NavLink.DropDownMenuNavLink;
+import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
 import com.gitblit.wicket.WicketUtils;
 
 public class DropDownMenu extends Panel {
 
 	private static final long serialVersionUID = 1L;
 
-	public DropDownMenu(String id, String label, final DropDownMenuRegistration menu) {
+	public DropDownMenu(String id, String label, final DropDownPageMenuNavLink menu) {
 		super(id);
 
 		add(new Label("label", label).setRenderBodyOnly(true));
-		ListDataProvider<MenuItem> items = new ListDataProvider<MenuItem>(
-				menu.menuItems);
+		ListDataProvider<MenuItem> items = new ListDataProvider<MenuItem>(menu.menuItems);
 		DataView<MenuItem> view = new DataView<MenuItem>("menuItems", items) {
 			private static final long serialVersionUID = 1L;
 
@@ -76,4 +76,39 @@
 		add(view);
 		setRenderBodyOnly(true);
 	}
+
+	public DropDownMenu(String id, String label, final DropDownMenuNavLink menu) {
+		super(id);
+
+		add(new Label("label", label).setRenderBodyOnly(true));
+		ListDataProvider<MenuItem> items = new ListDataProvider<MenuItem>(menu.menuItems);
+		DataView<MenuItem> view = new DataView<MenuItem>("menuItems", items) {
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			public void populateItem(final Item<MenuItem> item) {
+				MenuItem entry = item.getModelObject();
+				if (entry instanceof PageLinkMenuItem) {
+					// link to another Wicket page
+					PageLinkMenuItem pageLink = (PageLinkMenuItem) entry;
+					item.add(new LinkPanel("menuItem", null, null, pageLink.toString(), pageLink.getPageClass(),
+							pageLink.getPageParameters(), false).setRenderBodyOnly(true));
+				} else if (entry instanceof ExternalLinkMenuItem) {
+					// link to a specified href
+					ExternalLinkMenuItem extLink = (ExternalLinkMenuItem) entry;
+					item.add(new LinkPanel("menuItem", null, extLink.toString(), extLink.getHref(),
+							extLink.openInNewWindow()).setRenderBodyOnly(true));
+				} else if (entry instanceof MenuDivider) {
+					// divider
+					item.add(new Label("menuItem").setRenderBodyOnly(true));
+					WicketUtils.setCssClass(item, "divider");
+				} else {
+					throw new IllegalArgumentException(String.format("Unexpected menuitem type %s",
+							entry.getClass().getSimpleName()));
+				}
+			}
+		};
+		add(view);
+		setRenderBodyOnly(true);
+	}
 }
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/NavigationPanel.java b/src/main/java/com/gitblit/wicket/panels/NavigationPanel.java
index 7db29fa..2bc92f4 100644
--- a/src/main/java/com/gitblit/wicket/panels/NavigationPanel.java
+++ b/src/main/java/com/gitblit/wicket/panels/NavigationPanel.java
@@ -23,9 +23,11 @@
 import org.apache.wicket.markup.repeater.data.DataView;
 import org.apache.wicket.markup.repeater.data.ListDataProvider;
 
-import com.gitblit.wicket.PageRegistration;
-import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
-import com.gitblit.wicket.PageRegistration.OtherPageLink;
+import com.gitblit.models.NavLink;
+import com.gitblit.models.NavLink.DropDownMenuNavLink;
+import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
+import com.gitblit.models.NavLink.ExternalNavLink;
+import com.gitblit.models.NavLink.PageNavLink;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.pages.BasePage;
 
@@ -34,52 +36,59 @@
 	private static final long serialVersionUID = 1L;
 
 	public NavigationPanel(String id, final Class<? extends BasePage> pageClass,
-			List<PageRegistration> registeredPages) {
+			List<NavLink> navLinks) {
 		super(id);
 
-		ListDataProvider<PageRegistration> refsDp = new ListDataProvider<PageRegistration>(
-				registeredPages);
-		DataView<PageRegistration> refsView = new DataView<PageRegistration>("navLink", refsDp) {
+		ListDataProvider<NavLink> refsDp = new ListDataProvider<NavLink>(navLinks);
+		DataView<NavLink> linksView = new DataView<NavLink>("navLink", refsDp) {
 			private static final long serialVersionUID = 1L;
 
 			@Override
-			public void populateItem(final Item<PageRegistration> item) {
-				PageRegistration entry = item.getModelObject();
-				String linkText = entry.translationKey;
+			public void populateItem(final Item<NavLink> item) {
+				NavLink navLink = item.getModelObject();
+				String linkText = navLink.translationKey;
 				try {
 					// try to lookup translation key
-					linkText = getString(entry.translationKey);
+					linkText = getString(navLink.translationKey);
 				} catch (Exception e) {
 				}
 
-				if (entry.hiddenPhone) {
+				if (navLink.hiddenPhone) {
 					WicketUtils.setCssClass(item, "hidden-phone");
 				}
-				if (entry instanceof OtherPageLink) {
+				if (navLink instanceof ExternalNavLink) {
 					// other link
-					OtherPageLink link = (OtherPageLink) entry;
+					ExternalNavLink link = (ExternalNavLink) navLink;
 					Component c = new LinkPanel("link", null, linkText, link.url);
 					c.setRenderBodyOnly(true);
 					item.add(c);
-				} else if (entry instanceof DropDownMenuRegistration) {
+				} else if (navLink instanceof DropDownPageMenuNavLink) {
 					// drop down menu
-					DropDownMenuRegistration reg = (DropDownMenuRegistration) entry;
+					DropDownPageMenuNavLink reg = (DropDownPageMenuNavLink) navLink;
 					Component c = new DropDownMenu("link", linkText, reg);
 					c.setRenderBodyOnly(true);
 					item.add(c);
 					WicketUtils.setCssClass(item, "dropdown");
-				} else {
+				} else if (navLink instanceof DropDownMenuNavLink) {
+					// drop down menu
+					DropDownMenuNavLink reg = (DropDownMenuNavLink) navLink;
+					Component c = new DropDownMenu("link", linkText, reg);
+					c.setRenderBodyOnly(true);
+					item.add(c);
+					WicketUtils.setCssClass(item, "dropdown");
+				} else if (navLink instanceof PageNavLink) {
+					PageNavLink reg = (PageNavLink) navLink;
 					// standard page link
 					Component c = new LinkPanel("link", null, linkText,
-							entry.pageClass, entry.params);
+							reg.pageClass, reg.params);
 					c.setRenderBodyOnly(true);
-					if (entry.pageClass.equals(pageClass)) {
+					if (reg.pageClass.equals(pageClass)) {
 						WicketUtils.setCssClass(item, "active");
 					}
 					item.add(c);
 				}
 			}
 		};
-		add(refsView);
+		add(linksView);
 	}
 }
\ No newline at end of file
diff --git a/src/site/plugins_extensions.mkd b/src/site/plugins_extensions.mkd
index 18a7e32..7bf63c1 100644
--- a/src/site/plugins_extensions.mkd
+++ b/src/site/plugins_extensions.mkd
@@ -52,6 +52,37 @@
     public void onUninstall() {
     }
 }
+
+/**
+ * You can also create Webapp plugins that register mounted pages.
+ */
+public class ExampleWicketPlugin extends GitblitWicketPlugin {
+    @Override
+    public void start() {
+    }
+
+    @Override
+    public void stop() {
+    }
+
+    @Override
+    public void onInstall() {
+    }
+
+    @Override
+    public void onUpgrade(Version oldVersion) {
+    }
+
+    @Override
+    public void onUninstall() {
+    }
+
+    @Override
+    protected void init(GitblitWicketApp app) {
+        app.mount("/logo", LogoPage.class);
+        app.mount("/hello", HelloWorldPage.class);
+    }
+}
 ```
 
 ### SSH Dispatch Command
@@ -225,7 +256,32 @@
 
     @Override
     public List<MenuItem> getMenuItems(UserModel user) {
-        return Arrays.asList((MenuItem) new ExternalLinkMenuItem("Github", String.format("https://github.com/%s", user.username));
+        MenuItem item = new ExternalLinkMenuItem("Github", String.format("https://github.com/%s", user.username));
+        return Arrays.asList(item);
+    }
+}
+```
+
+### Navigation Links
+
+*SINCE 1.6.0*
+
+You can provide your own top-level navigation links by subclassing the *NavLinkExtension* class.
+
+```java
+import java.util.Arrays;
+import java.util.List;
+import ro.fortsoft.pf4j.Extension;
+import com.gitblit.extensions.NavLinkExtension;
+import com.gitblit.models.UserModel;
+
+@Extension
+public class MyNavLink extends NavLinkExtension {
+
+    @Override
+    public List<NavLink> getNavLinks(UserModel user) {
+        NavLink link = new ExternalLinkMenuItem("Github", String.format("https://github.com/%s", user.username));
+        return Arrays.asList(link);
     }
 }
 ```

--
Gitblit v1.9.1