/* * 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.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.OsgiManagerPlugin; 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; import org.osgi.service.log.LogService; /** * 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 = getPlugins(); for ( int i = 0; i < plugin.length; i++ ) { plugin[i].dispose(); } 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; } void addInternalPlugin( final OsgiManager osgiManager, final String pluginClassName, final String label) { final Plugin plugin = new InternalPlugin(this, osgiManager, pluginClassName, label); addPlugin( label, plugin ); } /** * 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, consolePlugin, label ); 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 ) { AbstractWebConsolePlugin consolePlugin = null; if ( label != null && label.length() > 0 ) { final Plugin plugin; synchronized ( plugins ) { plugin = ( Plugin ) plugins.get( label ); } if ( plugin != null ) { consolePlugin = plugin.getConsolePlugin(); } } else { Plugin[] plugins = getPlugins(); for ( int i = 0; i < plugins.length && consolePlugin == null; i++ ) { consolePlugin = plugins[i].getConsolePlugin(); } } return consolePlugin; } /** * 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. * <p> * The special entry {@code felix.webconsole.labelMap} is the flat, * unstructured map of labels to titles which is used as the * respective request attribute (see FELIX-3833). * * @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 String defaultCategory ) { final Map map = new HashMap(); final Map flatMap = new HashMap(); Plugin[] plugins = getPlugins(); for ( int i = 0; i < plugins.length; i++ ) { final Plugin plugin = plugins[i]; if ( !plugin.isEnabled() ) { continue; } // support only one level for now Map categoryMap = null; String category = plugin.getCategory(); if ( category == null || category.trim().length() == 0 ) { // FELIX-3798 configured default category category = defaultCategory; } // TODO: FELIX-3769; translate the Category categoryMap = findCategoryMap( map, category ); 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 */ } } categoryMap.put( label, title ); flatMap.put( label, title ); } // flat map of labels to titles (FELIX-3833) map.put( WebConsoleConstants.ATTR_LABEL_MAP, flatMap ); return map; } private Map findCategoryMap( Map map, String categoryPath ) { Map categoryMap = null; Map searchMap = map; String categories[] = categoryPath.split( "/" ); for ( int i = 0; i < categories.length; i++ ) { String categoryKey = "category." + categories[i]; if ( searchMap.containsKey( categoryKey ) ) { categoryMap = ( Map ) searchMap.get( categoryKey ); } else { categoryMap = new HashMap(); searchMap.put( categoryKey, categoryMap ); } searchMap = categoryMap; } return categoryMap; } /** * 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 ) { final Plugin[] plugin = getPlugins(); if ( servletContext != null ) { this.servletContext = servletContext; for ( int i = 0; i < plugin.length; i++ ) { try { plugin[i].init(); } catch ( ServletException se ) { // TODO: log !! } } } else { for ( int i = 0; i < plugin.length; i++ ) { try { plugin[i].destroy(); } catch (Throwable t) { // TODO: log !! } } this.servletContext = null; } } /** * 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) * * @see org.osgi.framework.ServiceListener#serviceChanged(org.osgi.framework.ServiceEvent) */ public void serviceChanged( ServiceEvent event ) { 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 ServletPlugin( 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 ) { synchronized ( plugins ) { plugins.put( label, plugin ); } } private void removePlugin( final String label ) { final Plugin oldPlugin; synchronized ( plugins ) { oldPlugin = ( Plugin ) plugins.remove( label ); } if ( oldPlugin != null ) { oldPlugin.dispose(); } } private Plugin[] getPlugins() { synchronized ( plugins ) { return ( Plugin[] ) plugins.values().toArray( new Plugin[plugins.size()] ); } } 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 class Plugin implements ServletConfig { private final PluginHolder holder; private final String label; private String title; private AbstractWebConsolePlugin consolePlugin; protected Plugin( final PluginHolder holder, final String label ) { this.holder = holder; this.label = label; } protected Plugin( final PluginHolder holder, final AbstractWebConsolePlugin plugin, final String label ) { this( holder, label ); if ( plugin == null ) { throw new NullPointerException( "plugin" ); } this.consolePlugin = plugin; } void init() throws ServletException { if (consolePlugin != null) { consolePlugin.init( this ); } } void destroy() { if (consolePlugin != null) { consolePlugin.destroy(); } } /** * Cleans up this plugin when it is not used any longer. This means * destroying the plugin servlet and, if it was registered as an OSGi * service, ungetting the service. */ final void dispose() { if ( consolePlugin != null ) { try { consolePlugin.destroy(); } catch ( Exception e ) { // TODO: handle } doUngetConsolePlugin( consolePlugin ); consolePlugin = null; } } protected PluginHolder getHolder() { return holder; } Bundle getBundle() { return getHolder().getBundleContext().getBundle(); } final String getLabel() { return label; } protected void setTitle( String title ) { this.title = title; } final String getTitle() { if ( title == null ) { final String title = doGetTitle(); this.title = ( title == null ) ? getLabel() : title; } return title; } protected String doGetTitle() { // get the service now final AbstractWebConsolePlugin consolePlugin = getConsolePlugin(); // reset the title: // - null if the servlet cannot be loaded // - to the servlet's actual title if the servlet is loaded return ( consolePlugin != null ) ? consolePlugin.getTitle() : null; } // methods added to support categories final String getCategory() { return doGetCategory(); } protected String doGetCategory() { // get the service now final AbstractWebConsolePlugin consolePlugin = getConsolePlugin(); return ( consolePlugin != null ) ? consolePlugin.getCategory() : null; } final AbstractWebConsolePlugin getConsolePlugin() { if ( consolePlugin == null ) { final AbstractWebConsolePlugin consolePlugin = doGetConsolePlugin(); if ( consolePlugin != null ) { try { this.consolePlugin = consolePlugin; init(); } catch ( ServletException se ) { // TODO: log this.consolePlugin = null; } } else { // TODO: log !! } } return consolePlugin; } protected boolean isEnabled() { return true; } protected AbstractWebConsolePlugin doGetConsolePlugin() { return consolePlugin; } protected void doUngetConsolePlugin( AbstractWebConsolePlugin consolePlugin ) { } //---------- ServletConfig interface public String getInitParameter( String name ) { return null; } public Enumeration getInitParameterNames() { return new Enumeration() { public boolean hasMoreElements() { return false; } public Object nextElement() { throw new NoSuchElementException(); } }; } public ServletContext getServletContext() { return getHolder().getServletContext(); } public String getServletName() { return getTitle(); } } private static class ServletPlugin extends Plugin { private final ServiceReference serviceReference; ServletPlugin( final PluginHolder holder, final ServiceReference serviceReference, final String label ) { super(holder, label); this.serviceReference = serviceReference; } Bundle getBundle() { return serviceReference.getBundle(); } protected String doGetTitle() { // check service Reference final String title = getProperty( serviceReference, WebConsoleConstants.PLUGIN_TITLE ); if ( title != null ) { return title; } // 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 setTitle(getLabel()); return super.doGetTitle(); } // added to support categories protected String doGetCategory() { // check service Reference final String category = getProperty( serviceReference, WebConsoleConstants.PLUGIN_CATEGORY ); if ( category != null ) { return category; } return super.doGetCategory(); } protected AbstractWebConsolePlugin doGetConsolePlugin() { Object service = getHolder().getBundleContext().getService( serviceReference ); if ( service instanceof Servlet ) { final AbstractWebConsolePlugin servlet; if ( service instanceof AbstractWebConsolePlugin ) { servlet = ( AbstractWebConsolePlugin ) service; } else { servlet = new WebConsolePluginAdapter( getLabel(), ( Servlet ) service, serviceReference ); } return servlet; } return null; } protected void doUngetConsolePlugin( AbstractWebConsolePlugin consolePlugin ) { getHolder().getBundleContext().ungetService( serviceReference ); } //---------- ServletConfig overwrite (based on ServletReference) public String getInitParameter( String name ) { Object property = serviceReference.getProperty( name ); if ( property != null && !property.getClass().isArray() ) { return property.toString(); } return super.getInitParameter( name ); } public Enumeration getInitParameterNames() { final String[] keys = 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(); } }; } } static class InternalPlugin extends Plugin { final String pluginClassName; final OsgiManager osgiManager; AbstractWebConsolePlugin plugin; boolean doLog = true; protected InternalPlugin(PluginHolder holder, OsgiManager osgiManager, String pluginClassName, String label) { super(holder, label); this.osgiManager = osgiManager; this.pluginClassName = pluginClassName; } protected final boolean isEnabled() { // check if the plugin is enabled return !osgiManager.isPluginDisabled(pluginClassName); } protected AbstractWebConsolePlugin doGetConsolePlugin() { if (null == plugin) { if (!isEnabled()) { if (doLog) { osgiManager.log( LogService.LOG_INFO, "Ignoring plugin " + pluginClassName + ": Disabled by configuration" ); doLog = false; } return null; } try { Class pluginClass = getClass().getClassLoader().loadClass(pluginClassName); plugin = (AbstractWebConsolePlugin) pluginClass.newInstance(); if (plugin instanceof OsgiManagerPlugin) { ((OsgiManagerPlugin) plugin).activate(getBundle().getBundleContext()); } doLog = true; // reset logging if it succeeded } catch (Throwable t) { plugin = null; // in case only activate has faled! if (doLog) { osgiManager.log( LogService.LOG_WARNING, "Failed to instantiate plugin " + pluginClassName, t ); doLog = false; } } } return plugin; } protected void doUngetConsolePlugin(AbstractWebConsolePlugin consolePlugin) { if (consolePlugin == plugin) plugin = null; if (consolePlugin instanceof OsgiManagerPlugin) { ((OsgiManagerPlugin) consolePlugin).deactivate(); } super.doUngetConsolePlugin(consolePlugin); } } }