James Moger
2014-04-22 7a401a3ff909bf82fb4068d6dba430497f74084a
Allow plugins to extend the top navbar and repository navbar

This change also ties the plugin manager into the Wicket framework and
allows plugins to contribute and mount new pages which are linked by the
top navbar and repository navbar extensions.
5 files added
1 files copied
1 files renamed
12 files modified
1 files deleted
983 ■■■■ changed files
src/main/java/com/gitblit/extensions/GitblitWicketPlugin.java 49 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/extensions/NavLinkExtension.java 10 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/extensions/RepositoryNavLinkExtension.java 12 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/models/NavLink.java 140 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitBlitWebApp.java 106 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitblitWicketApp.java 72 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/PageRegistration.java 99 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/PluginClassResolver.java 122 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/UrlExternalFormComparator.java 39 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/ActivityPage.java 10 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/DashboardPage.java 10 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/ProjectPage.java 14 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/ProjectsPage.java 10 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/RepositoriesPage.java 10 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/RepositoryPage.java 61 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/RootPage.java 57 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/UserPage.java 10 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/DropDownMenu.java 45 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/NavigationPanel.java 49 ●●●●● patch | view | raw | blame | history
src/site/plugins_extensions.mkd 58 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/extensions/GitblitWicketPlugin.java
New file
@@ -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);
}
src/main/java/com/gitblit/extensions/NavLinkExtension.java
File was renamed from src/main/java/com/gitblit/extensions/AdminMenuExtension.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);
}
src/main/java/com/gitblit/extensions/RepositoryNavLinkExtension.java
copy from src/main/java/com/gitblit/extensions/AdminMenuExtension.java copy to src/main/java/com/gitblit/extensions/RepositoryNavLinkExtension.java
File was copied from src/main/java/com/gitblit/extensions/AdminMenuExtension.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);
}
src/main/java/com/gitblit/models/NavLink.java
New file
@@ -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>();
        }
    }
}
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();
    }
src/main/java/com/gitblit/wicket/GitblitWicketApp.java
New file
@@ -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();
}
src/main/java/com/gitblit/wicket/PageRegistration.java
File was deleted
src/main/java/com/gitblit/wicket/PluginClassResolver.java
New file
@@ -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);
                }
            }
        }
    }
}
src/main/java/com/gitblit/wicket/UrlExternalFormComparator.java
New file
@@ -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());
    }
}
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);
    }
    /**
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);
    }
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
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);
    }
}
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) {
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() {
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) {
    }
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);
    }
}
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);
    }
}
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);
    }
}
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);
    }
}
```