FELIX-2246 Allow for lazy loading of WebConsole plugins

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@931511 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/WebConsoleConstants.java b/webconsole/src/main/java/org/apache/felix/webconsole/WebConsoleConstants.java
index a0950b8..daaf9aa 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/WebConsoleConstants.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/WebConsoleConstants.java
@@ -43,12 +43,18 @@
 
     /**
      * The title under which the OSGi Manager plugin is called by
-     * the OSGi Manager (value is "felix.webconsole.label").
+     * the OSGi Manager (value is "felix.webconsole.title").
      * <p>
      * For {@link #SERVICE_NAME Servlet} services not extending the
      * {@link AbstractWebConsolePlugin} this property is required for the
      * service to be used as a plugin. Otherwise the service is just ignored
      * by the Felix Web Console.
+     * <p>
+     * For {@link #SERVICE_NAME Servlet} services extending from the
+     * {@link AbstractWebConsolePlugin} abstract class this property is not
+     * technically required. To support lazy service access, e.g. for plugins
+     * implemented using the OSGi <i>Service Factory</i> pattern, the use
+     * of this service registration property is encouraged.
      *
      * @since 2.0.0
      */
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/WebConsolePluginAdapter.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/WebConsolePluginAdapter.java
index 3f841d1..0ad4a6e 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/WebConsolePluginAdapter.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/WebConsolePluginAdapter.java
@@ -48,9 +48,6 @@
     // the plugin label (aka address)
     private final String label;
 
-    // the plugin title rendered in the top bar
-    private final String title;
-
     // the actual plugin to forward rendering requests to
     private final Servlet plugin;
 
@@ -65,10 +62,9 @@
      * @param plugin the plugin itself
      * @param serviceReference reference to the plugin
      */
-    public WebConsolePluginAdapter( String label, String title, Servlet plugin, ServiceReference serviceReference )
+    public WebConsolePluginAdapter( String label, Servlet plugin, ServiceReference serviceReference )
     {
         this.label = label;
-        this.title = title;
         this.plugin = plugin;
         this.cssReferences = toStringArray( serviceReference.getProperty( WebConsoleConstants.PLUGIN_CSS_REFERENCES ) );
 
@@ -91,13 +87,17 @@
 
 
     /**
-     * Returns the title of this plugin page as defined in the constructor.
+     * Returns the title of this plugin page as defined by the
+     * {@link WebConsoleConstants#PLUGIN_TITLE} service registration attribute
+     * which is exposed as the servlet name in the servlet configuration.
      *
      * @see org.apache.felix.webconsole.AbstractWebConsolePlugin#getTitle()
      */
     public String getTitle()
     {
-        return title;
+        // return the servlet name from the configuration but don't call
+        // the base class implementation, which calls getTitle()
+        return getServletConfig().getServletName();
     }
 
 
@@ -190,6 +190,7 @@
         return requestUri.endsWith( ".html" ) || requestUri.lastIndexOf( '.' ) < 0;
     }
 
+
     /**
      * Directly refer to the plugin's service method unless the request method
      * is <code>GET</code> in which case we defer the call into the service method
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
index 294ac6b..f5df6a4 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
@@ -22,7 +22,6 @@
 import java.util.Collection;
 import java.util.Dictionary;
 import java.util.Enumeration;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Hashtable;
 import java.util.Iterator;
@@ -32,7 +31,6 @@
 import java.util.ResourceBundle;
 import java.util.Set;
 import javax.servlet.GenericServlet;
-import javax.servlet.Servlet;
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
@@ -43,12 +41,10 @@
 import org.apache.felix.webconsole.BrandingPlugin;
 import org.apache.felix.webconsole.WebConsoleConstants;
 import org.apache.felix.webconsole.internal.OsgiManagerPlugin;
-import org.apache.felix.webconsole.internal.WebConsolePluginAdapter;
 import org.apache.felix.webconsole.internal.core.BundlesServlet;
 import org.apache.felix.webconsole.internal.filter.FilteringResponseWrapper;
 import org.apache.felix.webconsole.internal.i18n.ResourceBundleManager;
 import org.apache.felix.webconsole.internal.misc.ConfigurationRender;
-import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.ServiceReference;
 import org.osgi.framework.ServiceRegistration;
@@ -125,8 +121,7 @@
     static final String DEFAULT_MANAGER_ROOT = "/system/console";
 
     static final String[] PLUGIN_CLASSES =
-        {
-            "org.apache.felix.webconsole.internal.compendium.ComponentConfigurationPrinter",
+        { "org.apache.felix.webconsole.internal.compendium.ComponentConfigurationPrinter",
             "org.apache.felix.webconsole.internal.compendium.ComponentsServlet",
             "org.apache.felix.webconsole.internal.compendium.ConfigManager",
             "org.apache.felix.webconsole.internal.compendium.ConfigurationAdminConfigurationPrinter",
@@ -140,8 +135,7 @@
             "org.apache.felix.webconsole.internal.misc.SystemPropertiesPrinter",
             "org.apache.felix.webconsole.internal.misc.ThreadPrinter",
             "org.apache.felix.webconsole.internal.obr.BundleRepositoryRender",
-            "org.apache.felix.webconsole.internal.system.VMStatPlugin"
-        };
+            "org.apache.felix.webconsole.internal.system.VMStatPlugin" };
 
     private BundleContext bundleContext;
 
@@ -149,24 +143,16 @@
 
     private HttpService httpService;
 
-    private ServiceTracker pluginsTracker;
+    private PluginHolder holder;
 
     private ServiceTracker brandingTracker;
 
     private ServiceRegistration configurationListener;
 
-    // map of plugins: indexed by the plugin label (String), values are
-    // AbstractWebConsolePlugin instances
-    private Map plugins = new HashMap();
-
     // list of OsgiManagerPlugin instances activated during init. All these
     // instances will have to be deactivated during destroy
     private List osgiManagerPlugins = new ArrayList();
 
-    private AbstractWebConsolePlugin defaultPlugin;
-
-    private String defaultRenderName;
-
     private String webManagerRoot;
 
     // true if the OsgiManager is registered as a Servlet with the HttpService
@@ -183,9 +169,11 @@
 
     private int logLevel = DEFAULT_LOG_LEVEL;
 
+
     public OsgiManager( BundleContext bundleContext )
     {
         this.bundleContext = bundleContext;
+        this.holder = new PluginHolder( bundleContext );
 
         updateConfiguration( null );
 
@@ -230,7 +218,6 @@
             configurationListener = null;
         }
 
-        this.defaultPlugin = null;
         this.bundleContext = null;
     }
 
@@ -242,6 +229,8 @@
         // base class initialization not needed, since the GenericServlet.init
         // is an empty method
 
+        holder.setServletContext( getServletContext() );
+
         // setup the included plugins
         ClassLoader classLoader = getClass().getClassLoader();
         for ( int i = 0; i < PLUGIN_CLASSES.length; i++ )
@@ -267,7 +256,7 @@
                 }
                 if ( plugin instanceof AbstractWebConsolePlugin )
                 {
-                    bindServlet( ( AbstractWebConsolePlugin ) plugin );
+                    holder.addOsgiManagerPlugin( ( AbstractWebConsolePlugin ) plugin );
                 }
                 else if ( plugin instanceof BrandingPlugin )
                 {
@@ -299,18 +288,20 @@
         resourceBundleManager = new ResourceBundleManager( getBundleContext() );
 
         // start the configuration render, providing the resource bundle manager
-        ConfigurationRender cr = new ConfigurationRender(resourceBundleManager);
+        ConfigurationRender cr = new ConfigurationRender( resourceBundleManager );
         cr.activate( bundleContext );
-        osgiManagerPlugins.add(cr);
-        bindServlet( cr );
+        osgiManagerPlugins.add( cr );
+        holder.addOsgiManagerPlugin( cr );
 
         // start tracking external plugins after setting up our own plugins
-        pluginsTracker = new PluginServiceTracker( this );
-        pluginsTracker.open();
-        brandingTracker = new BrandingServiceTracker(this);
+        holder.open();
+
+        // accept new console branding service
+        brandingTracker = new BrandingServiceTracker( this );
         brandingTracker.open();
     }
 
+
     public void service( ServletRequest req, ServletResponse res ) throws ServletException, IOException
     {
 
@@ -326,7 +317,7 @@
             {
                 path = path.concat( "/" );
             }
-            path = path.concat( defaultRenderName );
+            path = path.concat( holder.getDefaultPluginLabel() );
             response.sendRedirect( path );
             return;
         }
@@ -338,24 +329,25 @@
         }
 
         final String label = pathInfo.substring( 1, slash );
-        AbstractWebConsolePlugin plugin = ( AbstractWebConsolePlugin ) plugins.get( label );
+        AbstractWebConsolePlugin plugin = holder.getPlugin( label );
 
         if ( plugin == null )
         {
             if ( "install".equals( label ) )
             {
-                plugin = ( AbstractWebConsolePlugin ) plugins.get( BundlesServlet.NAME );
+                plugin = holder.getPlugin( BundlesServlet.NAME );
             }
         }
 
         if ( plugin != null )
         {
-            final Map labelMap = getLocalizedLabelMap( request.getLocale() );
+            final Map labelMap = holder.getLocalizedLabelMap( resourceBundleManager, request.getLocale() );
 
             // the official request attributes
             req.setAttribute( WebConsoleConstants.ATTR_LABEL_MAP, labelMap );
             req.setAttribute( WebConsoleConstants.ATTR_APP_ROOT, request.getContextPath() + request.getServletPath() );
-            req.setAttribute( WebConsoleConstants.ATTR_PLUGIN_ROOT, request.getContextPath() + request.getServletPath() + '/' + label);
+            req.setAttribute( WebConsoleConstants.ATTR_PLUGIN_ROOT, request.getContextPath() + request.getServletPath()
+                + '/' + label );
 
             // deprecated request attributes
             req.setAttribute( ATTR_LABEL_MAP_OLD, labelMap );
@@ -374,11 +366,15 @@
 
     }
 
+
     public void destroy()
     {
         // base class destroy not needed, since the GenericServlet.destroy
         // is an empty method
 
+        // dispose off held plugins
+        holder.close();
+
         // dispose off the resource bundle manager
         if ( resourceBundleManager != null )
         {
@@ -386,13 +382,8 @@
             resourceBundleManager = null;
         }
 
-        // stop listening for plugins
-        if ( pluginsTracker != null )
-        {
-            pluginsTracker.close();
-            pluginsTracker = null;
-        }
-        if( brandingTracker != null )
+        // stop listening for brandings
+        if ( brandingTracker != null )
         {
             brandingTracker.close();
             brandingTracker = null;
@@ -406,7 +397,6 @@
         }
 
         // simply remove all operations, we should not be used anymore
-        this.plugins.clear();
         this.osgiManagerPlugins.clear();
     }
 
@@ -516,113 +506,34 @@
         }
     }
 
-    private static class PluginServiceTracker extends ServiceTracker
+    private static class BrandingServiceTracker extends ServiceTracker
     {
-
         private final OsgiManager osgiManager;
 
 
-        PluginServiceTracker( OsgiManager osgiManager )
+        BrandingServiceTracker( OsgiManager osgiManager )
         {
-            super( osgiManager.getBundleContext(), WebConsoleConstants.SERVICE_NAME, null );
+            super( osgiManager.getBundleContext(), BrandingPlugin.class.getName(), null );
             this.osgiManager = osgiManager;
         }
 
 
         public Object addingService( ServiceReference reference )
         {
-            Object label = reference.getProperty( WebConsoleConstants.PLUGIN_LABEL );
-            if ( label instanceof String )
+            Object plugin = super.addingService( reference );
+            if ( plugin instanceof BrandingPlugin )
             {
-                Object operation = super.addingService( reference );
-                if ( operation instanceof Servlet )
-                {
-                    // wrap the servlet if it is not an AbstractWebConsolePlugin
-                    // but has a title in the service properties
-                    final AbstractWebConsolePlugin plugin;
-                    if ( operation instanceof AbstractWebConsolePlugin )
-                    {
-                        plugin = ( AbstractWebConsolePlugin ) operation;
-                    }
-                    else
-                    {
-
-                        // define the title from the PLUGIN_TITLE registration
-                        // property, the servlet name or the servlet "toString"
-                        Object title = reference.getProperty( WebConsoleConstants.PLUGIN_TITLE );
-                        if ( !( title instanceof String ) )
-                        {
-                            if ( operation instanceof GenericServlet )
-                            {
-                                title = ( ( GenericServlet ) operation ).getServletName();
-                            }
-
-                            if ( !( title instanceof String ) )
-                            {
-                                title = operation.toString();
-                            }
-                        }
-
-                        plugin = new WebConsolePluginAdapter( ( String ) label, ( String ) title,
-                            ( Servlet ) operation, reference );
-
-                        // ensure the AbstractWebConsolePlugin is correctly setup
-                        Bundle pluginBundle = reference.getBundle();
-                        plugin.activate( pluginBundle.getBundleContext() );
-                    }
-
-                    osgiManager.bindServlet( plugin );
-                }
-                return operation;
+                AbstractWebConsolePlugin.setBrandingPlugin( ( BrandingPlugin ) plugin );
             }
-
-            return null;
+            return plugin;
         }
 
 
         public void removedService( ServiceReference reference, Object service )
         {
-            Object label = reference.getProperty( WebConsoleConstants.PLUGIN_LABEL );
-            if ( label instanceof String )
-            {
-                // TODO: check reference properties !!
-                osgiManager.unbindServlet( ( String ) label );
-
-                // check whether the service is a WebConsolePluginAdapter in
-                // which case we have to deactivate it here (as we activated it
-                // while adding the service
-                if ( service instanceof WebConsolePluginAdapter )
-                {
-                    ( ( WebConsolePluginAdapter ) service ).deactivate();
-                }
-            }
-
-            super.removedService( reference, service );
-        }
-    }
-
-    private static class BrandingServiceTracker extends ServiceTracker
-    {
-        private final OsgiManager osgiManager;
-
-        BrandingServiceTracker( OsgiManager osgiManager ){
-            super( osgiManager.getBundleContext(), BrandingPlugin.class.getName(), null );
-            this.osgiManager = osgiManager;
-        }
-
-        public Object addingService( ServiceReference reference ){
-            Object plugin = super.addingService( reference );
-            if ( plugin instanceof BrandingPlugin )
-            {
-                AbstractWebConsolePlugin.setBrandingPlugin((BrandingPlugin) plugin);
-            }
-            return plugin;
-        }
-
-        public void removedService( ServiceReference reference, Object service ){
             if ( service instanceof BrandingPlugin )
             {
-                AbstractWebConsolePlugin.setBrandingPlugin(null);
+                AbstractWebConsolePlugin.setBrandingPlugin( null );
             }
             super.removedService( reference, service );
         }
@@ -635,8 +546,7 @@
         // do not bind service, when we are already bound
         if ( this.httpService != null )
         {
-            log( LogService.LOG_DEBUG,
-                "bindHttpService: Already bound to an HTTP Service, ignoring further services" );
+            log( LogService.LOG_DEBUG, "bindHttpService: Already bound to an HTTP Service, ignoring further services" );
             return;
         }
 
@@ -712,54 +622,6 @@
     }
 
 
-    private void bindServlet( final AbstractWebConsolePlugin plugin )
-    {
-        final String label = plugin.getLabel();
-        final String title = plugin.getTitle();
-        try
-        {
-            plugin.init( getServletConfig() );
-            plugins.put( label, plugin );
-
-            if ( this.defaultPlugin == null )
-            {
-                this.defaultPlugin = plugin;
-            }
-            else if ( label.equals( this.defaultRenderName ) )
-            {
-                this.defaultPlugin = plugin;
-            }
-        }
-        catch ( ServletException se )
-        {
-            log( LogService.LOG_WARNING, "Initialization of plugin '" + title + "' (" + label
-                + ") failed; not using this plugin", se );
-        }
-    }
-
-
-    private void unbindServlet( String label )
-    {
-        AbstractWebConsolePlugin plugin = ( AbstractWebConsolePlugin ) plugins.remove( label );
-        if ( plugin != null )
-        {
-            if ( this.defaultPlugin == plugin )
-            {
-                if ( this.plugins.isEmpty() )
-                {
-                    this.defaultPlugin = null;
-                }
-                else
-                {
-                    this.defaultPlugin = ( AbstractWebConsolePlugin ) plugins.values().iterator().next();
-                }
-            }
-
-            plugin.destroy();
-        }
-    }
-
-
     private Dictionary getConfiguration()
     {
         return configuration;
@@ -778,11 +640,8 @@
         logLevel = getProperty( config, PROP_LOG_LEVEL, DEFAULT_LOG_LEVEL );
         AbstractWebConsolePlugin.setLogLevel( logLevel );
 
-        defaultRenderName = getProperty( config, PROP_DEFAULT_RENDER, DEFAULT_PAGE );
-        if ( defaultRenderName != null && plugins.get( defaultRenderName ) != null )
-        {
-            defaultPlugin = ( AbstractWebConsolePlugin ) plugins.get( defaultRenderName );
-        }
+        // default plugin page configuration
+        holder.setDefaultPluginLabel( getProperty( config, PROP_DEFAULT_RENDER, DEFAULT_PAGE ) );
 
         // get the web manager root path
         String newWebManagerRoot = this.getProperty( config, PROP_MANAGER_ROOT, DEFAULT_MANAGER_ROOT );
@@ -923,43 +782,4 @@
         return stringConfig;
     }
 
-
-    /**
-     * Builds the map of labels to plugin titles to be stored as the
-     * <code>felix.webconsole.labelMap</code> request attribute. This map
-     * optionally localizes the plugin title using the providing bundle's
-     * resource bundle if the first character of the title is a percent
-     * sign (%). Titles not prefixed with a percent sign are added to the
-     * map unmodified.
-     *
-     * @param locale The locale to which the titles are to be localized
-     *
-     * @return The localized map of labels to titles
-     */
-    private final Map getLocalizedLabelMap( final Locale locale )
-    {
-        final Map map = new HashMap();
-        for ( Iterator pi = plugins.values().iterator(); pi.hasNext(); )
-        {
-            final AbstractWebConsolePlugin plugin = ( AbstractWebConsolePlugin ) pi.next();
-            final String label = plugin.getLabel();
-            String title = plugin.getTitle();
-            if ( title.startsWith( "%" ) )
-            {
-                try
-                {
-                    final ResourceBundle resourceBundle = resourceBundleManager.getResourceBundle( plugin.getBundle(),
-                        locale );
-                    title = resourceBundle.getString( title.substring( 1 ) );
-                }
-                catch ( Throwable e )
-                {
-                    /* ignore missing resource - use default title */
-                }
-            }
-            map.put( label, title );
-        }
-
-        return map;
-    }
 }
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/PluginHolder.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/PluginHolder.java
new file mode 100644
index 0000000..5cc921d
--- /dev/null
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/PluginHolder.java
@@ -0,0 +1,571 @@
+/*
+ * 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 org.apache.felix.webconsole.internal.servlet;
+
+
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.ResourceBundle;
+
+import javax.servlet.Servlet;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+
+import org.apache.felix.webconsole.AbstractWebConsolePlugin;
+import org.apache.felix.webconsole.WebConsoleConstants;
+import org.apache.felix.webconsole.internal.WebConsolePluginAdapter;
+import org.apache.felix.webconsole.internal.i18n.ResourceBundleManager;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+
+
+/**
+ * The <code>PluginHolder</code> class implements the maintenance and lazy
+ * access to web console plugin services.
+ */
+class PluginHolder implements ServiceListener
+{
+
+    // The Web Console's bundle context to access the plugin services
+    private final BundleContext bundleContext;
+
+    // registered plugins (Map<String label, Plugin plugin>)
+    private final Map plugins;
+
+    // The servlet context used to initialize plugin services
+    private ServletContext servletContext;
+
+    // the label of the default plugin
+    private String defaultPluginLabel;
+
+
+    PluginHolder( final BundleContext context )
+    {
+        this.bundleContext = context;
+        this.plugins = new HashMap();
+    }
+
+
+    //---------- OsgiManager support API
+
+    /**
+     * Start using the plugin manager with registration as a service listener
+     * and getting references to all plugins already registered in the
+     * framework.
+     */
+    void open()
+    {
+        try
+        {
+            bundleContext.addServiceListener( this, "(" + Constants.OBJECTCLASS + "="
+                + WebConsoleConstants.SERVICE_NAME + ")" );
+        }
+        catch ( InvalidSyntaxException ise )
+        {
+            // not expected, thus fail hard
+            throw new InternalError( "Failed registering for Servlet service events: " + ise.getMessage() );
+        }
+
+        try
+        {
+            ServiceReference[] refs = bundleContext.getServiceReferences( WebConsoleConstants.SERVICE_NAME, null );
+            if ( refs != null )
+            {
+                for ( int i = 0; i < refs.length; i++ )
+                {
+                    serviceAdded( refs[i] );
+                }
+            }
+        }
+        catch ( InvalidSyntaxException ise )
+        {
+            // not expected, thus fail hard
+            throw new InternalError( "Failed getting existing Servlet services: " + ise.getMessage() );
+        }
+    }
+
+
+    /**
+     * Stop using the plugin manager by removing as a service listener and
+     * releasing all held plugins, which includes ungetting and destroying any
+     * held plugin services.
+     */
+    void close()
+    {
+        bundleContext.removeServiceListener( this );
+
+        Plugin[] plugin = ( Plugin[] ) plugins.values().toArray( new Plugin[plugins.size()] );
+        for ( int i = 0; i < plugin.length; i++ )
+        {
+            plugin[i].ungetService();
+        }
+
+        plugins.clear();
+        defaultPluginLabel = null;
+    }
+
+
+    /**
+     * Returns label of the default plugin
+     * @return label of the default plugin
+     */
+    String getDefaultPluginLabel()
+    {
+        return defaultPluginLabel;
+    }
+
+
+    /**
+     * Sets the label of the default plugin
+     * @param defaultPluginLabel
+     */
+    void setDefaultPluginLabel( String defaultPluginLabel )
+    {
+        this.defaultPluginLabel = defaultPluginLabel;
+    }
+
+
+    /**
+     * Returns the default plugin as identified by the {@link #getDefaultPlugin()}
+     * or any plugin if no plugin is registered with that label
+     *
+     * @return The default plugin or <code>null</code> if no plugin is
+     *      registered at all
+     */
+    AbstractWebConsolePlugin getDefaultPlugin()
+    {
+        return getPlugin( defaultPluginLabel );
+    }
+
+
+    /**
+     * Adds an internal Web Console plugin
+     * @param consolePlugin The internal Web Console plugin to add
+     */
+    void addOsgiManagerPlugin( final AbstractWebConsolePlugin consolePlugin )
+    {
+        final String label = consolePlugin.getLabel();
+        final Plugin plugin = new Plugin( this, null, label );
+        plugin.setTitle( consolePlugin.getTitle() );
+        plugin.setConsolePlugin( consolePlugin );
+        addPlugin( label, plugin );
+    }
+
+
+    /**
+     * Remove the internal Web Console plugin registered under the given label
+     * @param label The label of the Web Console internal plugin to remove
+     */
+    void removeOsgiManagerPlugin( final String label )
+    {
+        removePlugin( label );
+    }
+
+
+    /**
+     * Returns the plugin registered under the given label or <code>null</code>
+     * if none is registered under that label. If the label is <code>null</code>
+     * or empty, any registered plugin is returned or <code>null</code> if
+     * no plugin is registered
+     *
+     * @param label The label of the plugin to return
+     * @return The plugin or <code>null</code> if no plugin is registered with
+     *      the given label.
+     */
+    AbstractWebConsolePlugin getPlugin( final String label )
+    {
+        if ( label == null || label.length() == 0 )
+        {
+            if ( !plugins.isEmpty() )
+            {
+                return ( ( Plugin ) plugins.values().iterator().next() ).getConsolePlugin();
+            }
+        }
+        else
+        {
+            Plugin plugin = ( Plugin ) plugins.get( label );
+            if ( plugin != null )
+            {
+                return plugin.getConsolePlugin();
+            }
+        }
+
+        // no such plugin (or not any more)
+        return null;
+    }
+
+
+    /**
+     * Builds the map of labels to plugin titles to be stored as the
+     * <code>felix.webconsole.labelMap</code> request attribute. This map
+     * optionally localizes the plugin title using the providing bundle's
+     * resource bundle if the first character of the title is a percent
+     * sign (%). Titles not prefixed with a percent sign are added to the
+     * map unmodified.
+     *
+     * @param resourceBundleManager The ResourceBundleManager providing
+     *      localized titles
+     * @param locale The locale to which the titles are to be localized
+     *
+     * @return The localized map of labels to titles
+     */
+    Map getLocalizedLabelMap( final ResourceBundleManager resourceBundleManager, final Locale locale )
+    {
+        final Map map = new HashMap();
+        for ( Iterator pi = plugins.values().iterator(); pi.hasNext(); )
+        {
+            final Plugin plugin = ( Plugin ) pi.next();
+            final String label = plugin.getLabel();
+            String title = plugin.getTitle();
+            if ( title.startsWith( "%" ) )
+            {
+                try
+                {
+                    final ResourceBundle resourceBundle = resourceBundleManager.getResourceBundle( plugin.getBundle(),
+                        locale );
+                    title = resourceBundle.getString( title.substring( 1 ) );
+                }
+                catch ( Throwable e )
+                {
+                    /* ignore missing resource - use default title */
+                }
+            }
+            map.put( label, title );
+        }
+
+        return map;
+    }
+
+
+    /**
+     * Returns the bundle context of the Web Console itself.
+     * @return the bundle context of the Web Console itself.
+     */
+    BundleContext getBundleContext()
+    {
+        return bundleContext;
+    }
+
+
+    /**
+     * Sets the servlet context to be used to initialize plugin services
+     * @param servletContext
+     */
+    void setServletContext( ServletContext servletContext )
+    {
+        this.servletContext = servletContext;
+    }
+
+
+    /**
+     * Returns the servlet context to be used to initialize plugin services
+     * @return the servlet context to be used to initialize plugin services
+     */
+    ServletContext getServletContext()
+    {
+        return servletContext;
+    }
+
+
+    //---------- ServletListener
+
+    /**
+     * Called when plugin services are registered or unregistered (or modified,
+     * which is currently ignored)
+     */
+    public void serviceChanged( ServiceEvent event )
+    {
+        final String label = getProperty( event.getServiceReference(), WebConsoleConstants.PLUGIN_LABEL );
+        if ( label != null )
+        {
+            switch ( event.getType() )
+            {
+                case ServiceEvent.REGISTERED:
+                    // add service
+                    serviceAdded( event.getServiceReference() );
+                    break;
+
+                case ServiceEvent.UNREGISTERING:
+                    // remove service
+                    serviceRemoved( event.getServiceReference() );
+                    break;
+
+                default:
+                    // update service
+                    break;
+            }
+        }
+    }
+
+
+    private void serviceAdded( final ServiceReference serviceReference )
+    {
+        final String label = getProperty( serviceReference, WebConsoleConstants.PLUGIN_LABEL );
+        if ( label != null )
+        {
+            addPlugin( label, new Plugin( this, serviceReference, label ) );
+        }
+    }
+
+
+    private void serviceRemoved( final ServiceReference serviceReference )
+    {
+        final String label = getProperty( serviceReference, WebConsoleConstants.PLUGIN_LABEL );
+        if ( label != null )
+        {
+            removePlugin( label );
+        }
+    }
+
+
+    private void addPlugin( final String label, final Plugin plugin )
+    {
+        plugins.put( label, plugin );
+    }
+
+
+    private void removePlugin( final String label )
+    {
+        final Plugin oldPlugin = ( Plugin ) plugins.remove( label );
+        if ( oldPlugin != null )
+        {
+            oldPlugin.ungetService();
+        }
+    }
+
+
+    static String getProperty( final ServiceReference service, final String propertyName )
+    {
+        final Object property = service.getProperty( propertyName );
+        if ( property instanceof String )
+        {
+            return ( String ) property;
+        }
+
+        return null;
+    }
+
+    private static final class Plugin implements ServletConfig
+    {
+        private final PluginHolder holder;
+        private final ServiceReference serviceReference;
+        private final String label;
+        private String title;
+        private AbstractWebConsolePlugin consolePlugin;
+
+
+        Plugin( final PluginHolder holder, final ServiceReference serviceReference, final String label )
+        {
+            this.holder = holder;
+            this.serviceReference = serviceReference;
+            this.label = label;
+        }
+
+
+        final Bundle getBundle()
+        {
+            if ( serviceReference != null )
+            {
+                return serviceReference.getBundle();
+            }
+            return holder.getBundleContext().getBundle();
+        }
+
+
+        final String getLabel()
+        {
+            return label;
+        }
+
+
+        void setTitle( String title )
+        {
+            this.title = title;
+        }
+
+
+        final String getTitle()
+        {
+            if ( title == null )
+            {
+                // assumption: serviceReference is only null for WebConsole
+                // internal plugins, for which the title field will always be set
+
+                // check service Reference
+                title = getProperty( serviceReference, WebConsoleConstants.PLUGIN_TITLE );
+                if ( title == null )
+                {
+                    // temporarily set the title to a non-null value to prevent
+                    // recursion issues if this method or the getServletName
+                    // method is called while the servlet is being acquired
+                    title = label;
+
+                    // get the service now
+                    acquireServlet();
+
+                    // reset the title:
+                    // - null if the servlet cannot be loaded
+                    // - to the servlet's actual title if the servlet is loaded
+                    title = ( consolePlugin != null ) ? consolePlugin.getTitle() : null;
+                }
+            }
+            return title;
+        }
+
+
+        final AbstractWebConsolePlugin getConsolePlugin()
+        {
+            acquireServlet();
+            return consolePlugin;
+        }
+
+
+        void setConsolePlugin( AbstractWebConsolePlugin service )
+        {
+            try
+            {
+                service.init( this );
+                this.consolePlugin = service;
+            }
+            catch ( ServletException se )
+            {
+                // TODO:
+                // log( LogService.LOG_WARNING, "Initialization of plugin '" + plugin.getTitle() + "' (" + plugin.getLabel()
+                //      + ") failed; not using this plugin", se );
+            }
+
+        }
+
+
+        final void ungetService()
+        {
+            if ( consolePlugin != null )
+            {
+                try
+                {
+                    consolePlugin.destroy();
+                }
+                catch ( Exception e )
+                {
+                    // TODO: handle
+                }
+                consolePlugin = null;
+
+                // service reference may be null for WebConsole internal plugins
+                if ( serviceReference != null )
+                {
+                    holder.getBundleContext().ungetService( serviceReference );
+                }
+            }
+        }
+
+
+        //---------- ServletConfig interface
+
+        public String getInitParameter( String name )
+        {
+            if ( serviceReference != null )
+            {
+                Object property = serviceReference.getProperty( name );
+                if ( property != null && !property.getClass().isArray() )
+                {
+                    return property.toString();
+                }
+            }
+
+            return null;
+        }
+
+
+        public Enumeration getInitParameterNames()
+        {
+            final String[] keys = ( serviceReference == null ) ? new String[0] : serviceReference.getPropertyKeys();
+            return new Enumeration()
+            {
+                int idx = 0;
+
+
+                public boolean hasMoreElements()
+                {
+                    return idx < keys.length;
+                }
+
+
+                public Object nextElement()
+                {
+                    if ( hasMoreElements() )
+                    {
+                        return keys[idx++];
+                    }
+                    throw new NoSuchElementException();
+                }
+
+            };
+        }
+
+
+        public ServletContext getServletContext()
+        {
+            return holder.getServletContext();
+        }
+
+
+        public String getServletName()
+        {
+            return getTitle();
+        }
+
+
+        private void acquireServlet()
+        {
+            if ( consolePlugin == null )
+            {
+                // assumption: serviceReference is only null for WebConsole
+                // internal plugins, for which the consolePlugin field will
+                // always be set
+
+                Object service = holder.getBundleContext().getService( serviceReference );
+                if ( service instanceof Servlet )
+                {
+                    final AbstractWebConsolePlugin servlet;
+                    if ( service instanceof AbstractWebConsolePlugin )
+                    {
+                        servlet = ( AbstractWebConsolePlugin ) service;
+                    }
+                    else
+                    {
+                        servlet = new WebConsolePluginAdapter( label, ( Servlet ) service, serviceReference );
+                    }
+
+                    setConsolePlugin( servlet );
+                }
+            }
+        }
+    }
+}