Fixed FELIX-2896 Add support for bundle info providers
https://issues.apache.org/jira/browse/FELIX-2896

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1373379 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/webconsole/pom.xml b/webconsole/pom.xml
index a066e12..ee94278 100644
--- a/webconsole/pom.xml
+++ b/webconsole/pom.xml
@@ -122,7 +122,9 @@
                             org.apache.felix.webconsole.internal.OsgiManagerActivator
                         </Bundle-Activator>
                         <Export-Package>
-                            org.apache.felix.webconsole;version=3.1.2;provide:=true
+                            org.apache.felix.webconsole;version=3.1.2;provide:=true,
+                            org.apache.felix.webconsole.bundleinfo;version=1.0.0;provide:=true,
+                            org.apache.felix.webconsole.i18n;version=1.0.0;provide:=true
                         </Export-Package>
                         <Import-Package>
                             org.osgi.service.metatype;resolution:=optional,
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/bundleinfo/BundleInfo.java b/webconsole/src/main/java/org/apache/felix/webconsole/bundleinfo/BundleInfo.java
new file mode 100644
index 0000000..e7f320e
--- /dev/null
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/bundleinfo/BundleInfo.java
@@ -0,0 +1,98 @@
+/*

+ * 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.bundleinfo;

+

+/**

+ * This entity defines additional bundle information entry, that is provided by

+ * the {@link BundleInfoProvider}. Each information entry is featured by name,

+ * value, type and description.

+ * 

+ * @author Valentin Valchev

+ */

+public class BundleInfo 

+{

+

+    private final String name;

+    private final String description;

+    private final Object value;

+    private final BundleInfoType type;

+

+    /**

+     * Creates a new bundle information entry.

+     * 

+     * @param name

+     *            the name of the entry

+     * @param value

+     *            the value associated with that entry

+     * @param type

+     *            the type of the value

+     * @param description

+     *            additional, user-friendly description for that value.

+     */

+    public BundleInfo(String name, Object value, BundleInfoType type,

+	    String description) 

+    {

+	this.name = name;

+	this.value = value;

+	this.type = type;

+	this.description = description;

+	type.validate(value);

+    }

+

+    /**

+     * Gets the name of the information entry. The name should be localized

+     * according the requested locale.

+     * 

+     * @return the name of that information key.

+     */

+    public String getName() 

+    {

+	return name;

+    }

+

+    /**

+     * Gets user-friendly description of the key pair. The description should be

+     * localized according the requested locale.

+     * 

+     * @return the description for that information key.

+     */

+    public String getDescription() 

+    {

+	return description;

+    }

+

+    /**

+     * Gets the information value.

+     * 

+     * @return the value.

+     */

+    public Object getValue() 

+    {

+	return value;

+    }

+

+    /**

+     * Gets the type of the information value.

+     * 

+     * @return the information type.

+     */

+    public BundleInfoType getType() 

+    {

+	return type;

+    }

+

+}

diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/bundleinfo/BundleInfoProvider.java b/webconsole/src/main/java/org/apache/felix/webconsole/bundleinfo/BundleInfoProvider.java
new file mode 100644
index 0000000..6a1f75a
--- /dev/null
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/bundleinfo/BundleInfoProvider.java
@@ -0,0 +1,70 @@
+/*

+ * 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.bundleinfo;

+

+import java.util.Locale;

+

+import org.osgi.framework.Bundle;

+

+/**

+ * The bundle info provider allows the user to supply additional information

+ * that will be used by the Web Console bundle plugin.

+ * 

+ * The API allows the user to register a special service, that could bind a

+ * custom, implementation-specific information to a bundle.

+ * 

+ * A typical use-case for that API would be the Declarative Services, that could

+ * provide information about the components provided by this bundle (and link to

+ * the component plugin too). Another usage could be the ProSyst resource

+ * manager, that would provide information about the memory and CPU usage of the

+ * bundle.

+ * 

+ * @author Valentin Valchev

+ */

+public interface BundleInfoProvider 

+{

+

+    /**

+     * This is just an utility - empty array, that could be returned when there

+     * is no additional information for a specific bundle.

+     */

+    public static final BundleInfo[] NO_INFO = new BundleInfo[0];

+

+    /**

+     * Gets the name of the bundle info provider as localized string.

+     * 

+     * @param locale

+     *            the locale in which the name should be returned

+     * @return the name of the bundle info provider.

+     */

+    String getName(Locale locale);

+

+    /**

+     * Gets the associated bundle information with the specified bundle (by it's

+     * ID)

+     * 

+     * @param bundle

+     *            the bundle, for which additional information is requested.

+     * @param webConsoleRoot

+     *            the root alias of the web console itself.

+     * @param locale

+     *            the locale in which the key-value pair should be returned.

+     * @return array of available {@link BundleInfo} or empty array if none.

+     */

+    BundleInfo[] getBundleInfo(Bundle bundle, String webConsoleRoot,

+	    Locale locale);

+}

diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/bundleinfo/BundleInfoType.java b/webconsole/src/main/java/org/apache/felix/webconsole/bundleinfo/BundleInfoType.java
new file mode 100644
index 0000000..bab6afa
--- /dev/null
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/bundleinfo/BundleInfoType.java
@@ -0,0 +1,108 @@
+/*

+ * 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.bundleinfo;

+

+import java.net.URL;

+

+/**

+ * This pre-java 5 enum defines all valid bundle information value types.

+ * 

+ * @author Valentin Valchev

+ */

+public final class BundleInfoType 

+{

+

+    /**

+     * Specifies that the value is {@link String} and is either a link to a

+     * local Servlet, or link to external HTTP server. In case the link starts

+     * with <code>&lt;protocol&gt;://</code> the link will be considered as

+     * external. Otherwise the link should be absolute link to a local Servlet

+     * and must always start with <code>/</code>.

+     * 

+     * for security reasons, the protocol cannot be <code>file</code> for

+     * external links.

+     */

+    public static final BundleInfoType LINK = new BundleInfoType("link"); //$NON-NLS-1$

+    /**

+     * This information type, specifies that the value of the information is URL

+     * object, that points to a resource. In that case the UI could consider

+     * that as a <em>download</em> link.

+     */

+    public static final BundleInfoType RESOURCE = new BundleInfoType("resource"); //$NON-NLS-1$

+    /**

+     * That information type is for normal information keys, that provide a

+     * normal (not link) value as information. The type of the value is

+     * <code>Object</code> and UI will visualize it by using it's

+     * {@link Object#toString()} method.

+     */

+    public static final BundleInfoType VALUE = new BundleInfoType("value"); //$NON-NLS-1$

+

+    private final String name;

+

+    private BundleInfoType(String name)

+    {

+	/* prevent instantiation */

+	this.name = name;

+    }

+

+    /**

+     * Returns the name of the type.

+     * 

+     * @return the type name

+     */

+    public final String getName()

+    {

+	return name;

+    }

+

+    /**

+     * That method is used to validate if the object is correct for the

+     * specified type.

+     * 

+     * @param value

+     *            the value that will be validated.

+     */

+    public final void validate(final Object value)

+    {

+	if (this == LINK)

+	{

+	    if (!(value instanceof String))

+		throw new IllegalArgumentException("Not a String");

+	    final String val = (String) value;

+	    final int idx = val.indexOf("://"); //$NON-NLS-1$

+	    // check local

+	    if (idx == -1)

+	    {

+		if (!val.startsWith("/")) //$NON-NLS-1$

+		    throw new IllegalArgumentException("Invalid local link");

+	    }

+	    else

+	    {

+		// check external link

+		if (val.substring(0, idx).equalsIgnoreCase("file")) //$NON-NLS-1$

+		    throw new IllegalArgumentException(

+			    "External link cannot use file protocol");

+	    }

+	}

+	else if (this == RESOURCE) 

+	{

+	    if (!(value instanceof URL))

+		throw new IllegalArgumentException("Invalid URL");

+	}

+    }

+

+}

diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/i18n/LocalizationHelper.java b/webconsole/src/main/java/org/apache/felix/webconsole/i18n/LocalizationHelper.java
new file mode 100644
index 0000000..a29b6c1
--- /dev/null
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/i18n/LocalizationHelper.java
@@ -0,0 +1,59 @@
+/*

+ * 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.i18n;

+

+import java.util.Locale;

+import java.util.ResourceBundle;

+

+import org.apache.felix.webconsole.internal.i18n.ResourceBundleCache;

+import org.osgi.framework.Bundle;

+

+/**

+ * The localization helper is supposed to be used from the bundle info

+ * providers. It will allow them to provide locale-specific names and

+ * descriptions of the bundle information entries.

+ * 

+ * @author Valentin Valchev

+ */

+public class LocalizationHelper {

+

+    private final ResourceBundleCache cache;

+

+    /**

+     * Creates a new helper instance.

+     * 

+     * @param bundle

+     *            the bundle that provides the localization resources. See the

+     *            standard OSGi-type localization support.

+     */

+    public LocalizationHelper(Bundle bundle) {

+	if (null == bundle)

+	    throw new NullPointerException();

+	this.cache = new ResourceBundleCache(bundle);

+    }

+

+    /**

+     * Used to retrieve the resource bundle for the specified locale.

+     * 

+     * @param locale

+     *            the requested locale.

+     * @return the resource bundle (could be empty, but never <code>null</code>)

+     */

+    public ResourceBundle getResourceBundle(final Locale locale) {

+	return cache.getResourceBundle(locale);

+    }

+}

diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java
index 2bbdf49..43ed413 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java
@@ -32,6 +32,9 @@
 import org.apache.felix.utils.manifest.Clause;
 import org.apache.felix.utils.manifest.Parser;
 import org.apache.felix.webconsole.*;
+import org.apache.felix.webconsole.bundleinfo.BundleInfo;
+import org.apache.felix.webconsole.bundleinfo.BundleInfoProvider;
+import org.apache.felix.webconsole.bundleinfo.BundleInfoType;
 import org.apache.felix.webconsole.internal.OsgiManagerPlugin;
 import org.apache.felix.webconsole.internal.Util;
 import org.json.*;
@@ -42,6 +45,7 @@
 import org.osgi.service.packageadmin.ExportedPackage;
 import org.osgi.service.packageadmin.PackageAdmin;
 import org.osgi.service.startlevel.StartLevel;
+import org.osgi.util.tracker.ServiceTracker;
 
 
 /**
@@ -81,6 +85,7 @@
     private boolean[] bootPkgWildcards;
 
     private ServiceRegistration configurationPrinter;
+    private ServiceTracker bundleInfoTracker;
 
     // templates
     private final String TEMPLATE_MAIN;
@@ -100,6 +105,9 @@
     public void activate( BundleContext bundleContext )
     {
         super.activate( bundleContext );
+        
+        bundleInfoTracker = new ServiceTracker( bundleContext, BundleInfoProvider.class.getName(), null);
+        bundleInfoTracker.open();
 
         // bootdelegation property parsing from Apache Felix R4SearchPolicyCore
         String bootDelegation = bundleContext.getProperty( Constants.FRAMEWORK_BOOTDELEGATION );
@@ -135,6 +143,12 @@
             configurationPrinter.unregister();
             configurationPrinter = null;
         }
+        
+        if ( bundleInfoTracker != null)
+        {
+            bundleInfoTracker.close();
+            bundleInfoTracker = null;
+        }
 
         super.deactivate();
     }
@@ -758,10 +772,53 @@
         }
 
         listHeaders( jw, bundle );
+        bundleInfoDetails(jw, bundle, pluginRoot.substring(0, pluginRoot.lastIndexOf("/")), locale);
 
         jw.endArray();
     }
 
+    
+    private final void bundleInfoDetails( JSONWriter jw, Bundle bundle, String appRoot, final Locale locale)
+	        throws JSONException
+    {
+	jw.object();
+	jw.key( "key" );
+	jw.value( "nfo" );
+	jw.key( "value");
+	jw.object();
+        final Object[] bundleInfoProviders = bundleInfoTracker.getServices();
+        for ( int i = 0; bundleInfoProviders != null && i < bundleInfoProviders.length; i++ )
+        {
+            final BundleInfoProvider infoProvider = (BundleInfoProvider) bundleInfoProviders[i];
+            final BundleInfo[] infos = infoProvider.getBundleInfo(bundle, appRoot, locale);
+            if ( null != infos && infos.length > 0)
+            {
+        	jw.key( infoProvider.getName(locale) );
+        	jw.array();
+        	for ( int j = 0; j < infos.length; j++ ) 
+        	{
+        	    bundleInfo( jw, infos[j] );
+        	}
+        	jw.endArray();
+            }
+        }
+        jw.endObject(); // value
+        jw.endObject();
+    }
+    
+    private static final void bundleInfo( JSONWriter jw, BundleInfo info ) throws JSONException
+    {
+	jw.object();
+	jw.key("name");
+	jw.value( info.getName() );
+	jw.key("description");
+	jw.value( info.getDescription() );
+	jw.key("type");
+	jw.value( info.getType().getName() );
+	jw.key("value");
+	jw.value( info.getValue() );
+	jw.endObject();
+    }
 
     private final Integer getStartLevel( Bundle bundle )
     {
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java
index 512f674..2a41111 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java
@@ -38,9 +38,11 @@
 import org.json.JSONException;
 import org.json.JSONWriter;
 import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
 import org.osgi.framework.Constants;
 import org.osgi.framework.InvalidSyntaxException;
 import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
 
 
 /**
@@ -104,8 +106,8 @@
     }
 
     /** the label for the services plugin */
-    public static final String LABEL = "services";
-    private static final String TITLE = "%services.pluginTitle";
+    public static final String LABEL = "services"; //$NON-NLS-1$
+    private static final String TITLE = "%services.pluginTitle"; //$NON-NLS-1$
     private static final String CSS[] = null;
 
     private final String TEMPLATE;
@@ -115,7 +117,24 @@
         super(LABEL, TITLE, CSS);
 
         // load templates
-        TEMPLATE = readTemplateFile( "/templates/services.html" );
+        TEMPLATE = readTemplateFile( "/templates/services.html" ); //$NON-NLS-1$
+    }
+    
+    private ServiceRegistration bipReg;
+    
+    public void activate(BundleContext bundleContext) 
+    {
+        super.activate(bundleContext);
+        bipReg = new ServicesUsedInfoProvider( bundleContext.getBundle() ).register( bundleContext );
+    }
+    
+    public void deactivate() {
+	if ( null != bipReg )
+	{
+	    bipReg.unregister();
+	    bipReg = null;
+	}
+        super.deactivate();
     }
 
 
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesUsedInfoProvider.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesUsedInfoProvider.java
new file mode 100644
index 0000000..fb784b0
--- /dev/null
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesUsedInfoProvider.java
@@ -0,0 +1,94 @@
+/*

+ * 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.core;

+

+import java.text.MessageFormat;

+import java.util.Arrays;

+import java.util.Locale;

+

+import org.apache.felix.webconsole.bundleinfo.BundleInfo;

+import org.apache.felix.webconsole.bundleinfo.BundleInfoProvider;

+import org.apache.felix.webconsole.bundleinfo.BundleInfoType;

+import org.apache.felix.webconsole.i18n.LocalizationHelper;

+import org.osgi.framework.Bundle;

+import org.osgi.framework.BundleContext;

+import org.osgi.framework.Constants;

+import org.osgi.framework.ServiceReference;

+import org.osgi.framework.ServiceRegistration;

+

+final class ServicesUsedInfoProvider implements

+	BundleInfoProvider {

+

+    // TODO: add i18n for those entries

+    private static final String SERVICE_DESCRIPTION = "%services.info.descr.";

+    

+    private final LocalizationHelper localization;

+    

+    ServicesUsedInfoProvider(Bundle bundle) 

+    {

+	localization = new LocalizationHelper(bundle);

+    }

+

+    /*

+     * (non-Javadoc)

+     * 

+     * @see

+     * org.apache.felix.webconsole.bundleinfo.BundleInfoProvider#getName(java

+     * .util.Locale)

+     */

+    public String getName(Locale locale) 

+    {

+	return localization.getResourceBundle(locale).getString("services.info.name"); //$NON-NLS-1$;

+    }

+

+    public BundleInfo[] getBundleInfo(Bundle bundle, String webConsoleRoot,

+	    Locale locale) 

+    {

+	final ServiceReference[] refs = bundle.getServicesInUse();

+	if (null == refs || refs.length == 0)

+	    return NO_INFO;

+	

+	BundleInfo[] ret = new BundleInfo[refs.length];

+	for ( int i=0; i < refs.length; i++ )

+	{

+	    ret[i] = toInfo(refs[i], webConsoleRoot, locale);

+	}

+	return ret;

+    }

+

+    private BundleInfo toInfo(ServiceReference ref, String webConsoleRoot, Locale locale) 

+    {

+	final String[] classes = (String[]) ref

+		.getProperty(Constants.OBJECTCLASS);

+	final Object id = ref.getProperty(Constants.SERVICE_ID);

+	final String descr =  localization.getResourceBundle(locale).getString("services.info.descr"); //$NON-NLS-1$;

+	String name = localization.getResourceBundle(locale).getString("services.info.key"); //$NON-NLS-1$;

+	name = MessageFormat.format(name, new Object[] {

+		id, Arrays.asList(classes).toString()

+	});

+	return new BundleInfo(name, webConsoleRoot + "/services/" + id, //$NON-NLS-1$

+		BundleInfoType.LINK, descr);

+    }

+    

+    ServiceRegistration register( BundleContext context )

+    {

+	return context.registerService(BundleInfoProvider.class.getName(), this, null);

+    }

+

+}

diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/i18n/ResourceBundleCache.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/i18n/ResourceBundleCache.java
index bf3514e..9962d20 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/i18n/ResourceBundleCache.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/i18n/ResourceBundleCache.java
@@ -33,12 +33,12 @@
 /**
  * The <code>ResourceBundleCache</code> caches resource bundles per OSGi bundle.
  */
-class ResourceBundleCache
+public class ResourceBundleCache
 {
 
     /**
      * The default locale corresponding to the default language in the
-     * bundle.properties file, which is english.
+     * bundle.properties file, which is English.
      * (FELIX-1957 The Locale(String) constructor used before is not available
      * in the OSGi/Minimum-1.1 profile and should be prevented)
      */
@@ -51,14 +51,25 @@
     private Map resourceBundleEntries;
 
 
-    ResourceBundleCache( final Bundle bundle )
+    /**
+     * Creates a new object
+     * 
+     * @param bundle the bundle which resources should be loaded.
+     */
+    public ResourceBundleCache( final Bundle bundle )
     {
         this.bundle = bundle;
         this.resourceBundles = new HashMap();
     }
 
 
-    ResourceBundle getResourceBundle( final Locale locale )
+    /**
+     * Gets the resource bundle for the specified locale.
+     * 
+     * @param locale the requested locale
+     * @return the resource bundle for the requested locale
+     */
+    public ResourceBundle getResourceBundle( final Locale locale )
     {
         if ( locale == null )
         {
@@ -161,7 +172,7 @@
     }
 
 
-    private Locale getParentLocale( Locale locale )
+    private static final Locale getParentLocale( Locale locale )
     {
         if ( locale.getVariant().length() != 0 )
         {
diff --git a/webconsole/src/main/resources/OSGI-INF/l10n/bundle.properties b/webconsole/src/main/resources/OSGI-INF/l10n/bundle.properties
index 696504c..f6b8218 100644
--- a/webconsole/src/main/resources/OSGI-INF/l10n/bundle.properties
+++ b/webconsole/src/main/resources/OSGI-INF/l10n/bundle.properties
@@ -78,6 +78,9 @@
 services.statusline=Services information: {0} service(s) in total.
 services.caption=Services
 services.usingBundles=Using Bundles
+services.info.name=Used Services
+services.info.descr=This is a OSGi service that is used by the current bundle. Click to see more details in 'Services' plugin.
+services.info.key=Service #{0} of type(s) {1}
 
 # Log plugin
 logs.pluginTitle=Log Service
diff --git a/webconsole/src/main/resources/OSGI-INF/l10n/bundle_bg.properties b/webconsole/src/main/resources/OSGI-INF/l10n/bundle_bg.properties
index 2910a8a..fbf4136 100644
--- a/webconsole/src/main/resources/OSGI-INF/l10n/bundle_bg.properties
+++ b/webconsole/src/main/resources/OSGI-INF/l10n/bundle_bg.properties
@@ -78,6 +78,10 @@
 services.statusline=Информация за услугите: {0} услуги.
 services.caption=Услуги
 services.usingBundles=Използващи бъндъли
+services.info.name=Използвани услуги
+services.info.descr=Тази услуга се използва от избраният бъндъл. Кликнете за да видите повече детайли в плъгин "Услуги"
+services.info.key=Услуга #{0} от тип(ове) {1}
+
 
 # Log plugin
 logs.pluginTitle=Журнал
diff --git a/webconsole/src/main/resources/res/ui/bundles.js b/webconsole/src/main/resources/res/ui/bundles.js
index d316b38..703b961 100644
--- a/webconsole/src/main/resources/res/ui/bundles.js
+++ b/webconsole/src/main/resources/res/ui/bundles.js
@@ -175,39 +175,64 @@
     var details = data.props;
     for (var idx in details) {
         var prop = details[idx];
-		var key = i18n[prop.key] ? i18n[prop.key] : prop.key;
-
-        var txt = "<tr><td class='aligntop' noWrap='true' style='border:0px none'>" + key + "</td><td class='aligntop' style='border:0px none'>";          
-        if (prop.value) {
-            if ( prop.key == 'Bundle Documentation' )  {
-                txt = txt + "<a href='" + prop.value + "' target='_blank'>" + prop.value + "</a>";
-            } else  {
-                if ( $.isArray(prop.value) ) {
-                    var i = 0;
-                    for(var pi in prop.value) {
-                        var value = prop.value[pi];
-                        if (i > 0) { txt = txt + "<br/>"; }
-		                var span;
-		                if (value.substring(0, 6) == "INFO: ") {
-		                	txt = txt + "<span class='ui-state-info-text'>" + value.substring(5) + "</span>";
-		                } else if (value.substring(0, 7) == "ERROR: ") {
-		                	txt = txt + "<span class='ui-state-error-text'>" + value.substring(6) + "</span>";
-		                } else {
-		                	txt = txt + value;
-		                }
-                        i++;
-                    }
-                } else {
-                    txt = txt + prop.value;
-                }
-            }
-        } else {
-            txt = txt + "\u00a0";
-        }
-        txt = txt + "</td></tr>";
-        $("#pluginInlineDetails" + data.id + " > table > tbody").append(txt);
+        
+        if (prop.key == 'nfo') {
+        	$.each(prop.value, function(name, bundleInfo) {
+        		var txt = '';
+        		$.each(bundleInfo, function(idx, ie) {
+        			txt += '<div title="' + makeSafe(ie.description) + '">';
+        			if (ie.type == 'link' || ie.type == 'resource') {
+        				txt += '<a href="' + ie.value + '">' + ie.name + '</a>';
+        			} else {
+        				txt += ie.name + " = " + ie.value;
+        			}
+        			txt += '</div>';
+        		});
+            	$("#pluginInlineDetails" + data.id + " > table > tbody").append( 
+                		renderDetailsEntry(name, txt) );
+        	});
+        } else 
+        	$("#pluginInlineDetails" + data.id + " > table > tbody").append( 
+        		renderDetailsEntry(prop.key, prop.value) );
     }
 }
+function makeSafe(text) {
+	return text.replace(/\W/g, function (chr) {
+		return '&#' + chr.charCodeAt(0) + ';';
+	});
+};
+
+function renderDetailsEntry(key, value) {
+	var key18 = i18n[key] ? i18n[key] : key;
+	var txt = "<tr><td class='aligntop' noWrap='true' style='border:0px none'>" + key18 + "</td><td class='aligntop' style='border:0px none'>";          
+    if (value) {
+        if ( key == 'Bundle Documentation' )  {
+            txt += "<a href='" + value + "' target='_blank'>" + value + "</a>";
+        } else  {
+            if ( $.isArray(value) ) {
+                var i = 0;
+                for(var pi in value) {
+                    var xv = value[pi];
+                    if (i > 0) { txt = txt + "<br/>"; }
+	                var span;
+	                if (xv.substring(0, 6) == "INFO: ") {
+	                	txt += "<span class='ui-state-info-text'>" + xv.substring(5) + "</span>";
+	                } else if (xv.substring(0, 7) == "ERROR: ") {
+	                	txt += "<span class='ui-state-error-text'>" + xv.substring(6) + "</span>";
+	                } else {
+	                	txt +=  xv;
+	                }
+                    i++;
+                }
+            } else {
+                txt += value;
+            }
+        }
+    } else {
+        txt += "\u00a0";
+    }
+    return txt + "</td></tr>";
+}
 
 
 $(document).ready(function(){