FELIX-1884 Apply slightly modified patch by Justin Edelson (thanks alot). The modification is that the ConfigurationPrinter output also includes references to the using bundles. Since the new ServicesServlet adds a ConfigurationPrinter for services, the service listing of the ConfigurationRender class is now removed.

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@884540 13f79535-47bb-0310-9956-ffa450edef68
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 b24f1a5..5315a59 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
@@ -148,7 +148,7 @@
         try
         {
             StringWriter w = new StringWriter();
-            writeJSON( w, null, null, true );
+            writeJSON( w, null, null, null, true );
             String jsonString = w.toString();
             JSONObject json = new JSONObject( jsonString );
 
@@ -224,7 +224,8 @@
         if ( reqInfo.extension.equals("json")  )
         {
             final String pluginRoot = ( String ) request.getAttribute( WebConsoleConstants.ATTR_PLUGIN_ROOT );
-            this.renderJSON(response, reqInfo.bundle, pluginRoot);
+            final String servicesRoot = getServicesRoot( request );
+            this.renderJSON(response, reqInfo.bundle, pluginRoot, servicesRoot);
 
             // nothing more to do
             return;
@@ -233,7 +234,6 @@
         super.doGet( request, response );
     }
 
-
     protected void doPost( HttpServletRequest req, HttpServletResponse resp ) throws ServletException, IOException
     {
         final RequestInfo reqInfo = new RequestInfo(req);
@@ -324,14 +324,20 @@
                 // we ignore this
             }
             final String pluginRoot = ( String ) req.getAttribute( WebConsoleConstants.ATTR_PLUGIN_ROOT );
-            this.renderJSON(resp, null, pluginRoot);
+            final String servicesRoot = getServicesRoot( req );
+            this.renderJSON(resp, null, pluginRoot, servicesRoot);
         }
         else
         {
             super.doPost( req, resp );
         }
     }
-
+    
+    private String getServicesRoot(HttpServletRequest request)
+    {
+        return ( ( String ) request.getAttribute( WebConsoleConstants.ATTR_APP_ROOT ) ) +
+            "/" + ServicesServlet.LABEL + "/";
+    }
 
     private Bundle getBundle( String pathInfo )
     {
@@ -418,7 +424,8 @@
             Util.startScript( pw );
             pw.print( "renderBundles(");
             final String pluginRoot = ( String ) request.getAttribute( WebConsoleConstants.ATTR_PLUGIN_ROOT );
-            writeJSON(pw, reqInfo.bundle, pluginRoot );
+            final String servicesRoot = getServicesRoot ( request );
+            writeJSON(pw, reqInfo.bundle, pluginRoot, servicesRoot );
             pw.println(");" );
             Util.endScript( pw );
         }
@@ -437,24 +444,26 @@
         pw.println( "</form></div");
     }
 
-    private void renderJSON( final HttpServletResponse response, final Bundle bundle, final String pluginRoot ) throws IOException
+    private void renderJSON( final HttpServletResponse response, final Bundle bundle, final String pluginRoot, final String servicesRoot )
+        throws IOException
     {
         response.setContentType( "application/json" );
         response.setCharacterEncoding( "UTF-8" );
 
         final PrintWriter pw = response.getWriter();
-        writeJSON(pw, bundle, pluginRoot);
+        writeJSON(pw, bundle, pluginRoot, servicesRoot);
     }
 
 
-    private void writeJSON( final PrintWriter pw, final Bundle bundle, final String pluginRoot ) throws IOException
+    private void writeJSON( final PrintWriter pw, final Bundle bundle, final String pluginRoot, final String servicesRoot )
+        throws IOException
     {
-        writeJSON( pw, bundle, pluginRoot, false );
+        writeJSON( pw, bundle, pluginRoot, servicesRoot, false );
     }
 
 
     private void writeJSON( final Writer pw, final Bundle bundle, final String pluginRoot,
-        final boolean fullDetails ) throws IOException
+        final String servicesRoot, final boolean fullDetails ) throws IOException
     {
         final Bundle[] allBundles = this.getBundles();
         final String statusLine = this.getStatusLine(allBundles);
@@ -477,7 +486,7 @@
 
             for ( int i = 0; i < bundles.length; i++ )
             {
-                bundleInfo( jw, bundles[i], fullDetails || bundle != null, pluginRoot );
+                bundleInfo( jw, bundles[i], fullDetails || bundle != null, pluginRoot, servicesRoot );
             }
 
             jw.endArray();
@@ -552,7 +561,8 @@
         return buffer.toString();
     }
 
-    private void bundleInfo( JSONWriter jw, Bundle bundle, boolean details, final String pluginRoot ) throws JSONException
+    private void bundleInfo( JSONWriter jw, Bundle bundle, boolean details, final String pluginRoot, final String servicesRoot )
+        throws JSONException
     {
         jw.object();
         jw.key( "id" );
@@ -587,7 +597,7 @@
 
         if ( details )
         {
-            bundleDetails( jw, bundle, pluginRoot );
+            bundleDetails( jw, bundle, pluginRoot, servicesRoot );
         }
 
         jw.endObject();
@@ -669,7 +679,8 @@
     }
 
 
-    private void bundleDetails( JSONWriter jw, Bundle bundle, final String pluginRoot ) throws JSONException
+    private void bundleDetails( JSONWriter jw, Bundle bundle, final String pluginRoot, final String servicesRoot)
+        throws JSONException
     {
         Dictionary headers = bundle.getHeaders();
 
@@ -705,7 +716,7 @@
             listImportExport( jw, bundle, pluginRoot );
         }
 
-        listServices( jw, bundle );
+        listServices( jw, bundle, servicesRoot );
 
         listHeaders( jw, bundle );
 
@@ -944,9 +955,23 @@
             }
         }
     }
+    
+    private String getServiceID(ServiceReference ref, final String servicesRoot) {
+        String id = ref.getProperty( Constants.SERVICE_ID ).toString();
+        StringBuffer val = new StringBuffer();
+        
+        if ( servicesRoot != null ) {
+            val.append( "<a href='" ).append( servicesRoot ).append( id ).append( "'>" );
+            val.append( id );
+            val.append( "</a>" );
+            return val.toString();
+        } else {
+            return id;
+        }
+    }
 
 
-    private void listServices( JSONWriter jw, Bundle bundle ) throws JSONException
+    private void listServices( JSONWriter jw, Bundle bundle, final String servicesRoot ) throws JSONException
     {
         ServiceReference[] refs = bundle.getRegisteredServices();
         if ( refs == null || refs.length == 0 )
@@ -956,7 +981,9 @@
 
         for ( int i = 0; i < refs.length; i++ )
         {
-            String key = "Service ID " + refs[i].getProperty( Constants.SERVICE_ID );
+            
+            
+            String key = "Service ID " + getServiceID( refs[i], servicesRoot );
 
             JSONArray val = new JSONArray();
 
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
new file mode 100644
index 0000000..7250306
--- /dev/null
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java
@@ -0,0 +1,515 @@
+/*
+ * 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.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.text.MessageFormat;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.felix.webconsole.ConfigurationPrinter;
+import org.apache.felix.webconsole.WebConsoleConstants;
+import org.apache.felix.webconsole.internal.BaseWebConsolePlugin;
+import org.apache.felix.webconsole.internal.Util;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+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;
+import org.osgi.service.log.LogService;
+
+
+public class ServicesServlet extends BaseWebConsolePlugin implements ConfigurationPrinter
+{
+
+    private final class RequestInfo
+    {
+        public final String extension;
+        public final ServiceReference service;
+        public final boolean serviceRequested;
+        public final String pathInfo;
+
+
+        protected RequestInfo( final HttpServletRequest request )
+        {
+            String info = request.getPathInfo();
+            // remove label and starting slash
+            info = info.substring( getLabel().length() + 1 );
+
+            // get extension
+            if ( info.endsWith( ".json" ) )
+            {
+                extension = "json";
+                info = info.substring( 0, info.length() - 5 );
+            }
+            else
+            {
+                extension = "html";
+            }
+
+            // we only accept direct requests to a service if they have a slash
+            // after the label
+            String serviceInfo = null;
+            if ( info.startsWith( "/" ) )
+            {
+                serviceInfo = info.substring( 1 );
+            }
+            if ( serviceInfo == null || serviceInfo.length() == 0 )
+            {
+                service = null;
+                serviceRequested = false;
+                pathInfo = null;
+            }
+            else
+            {
+                service = getServiceById( serviceInfo );
+                serviceRequested = true;
+                pathInfo = serviceInfo;
+            }
+            request.setAttribute( ServicesServlet.class.getName(), this );
+        }
+
+    }
+
+
+    public static RequestInfo getRequestInfo( final HttpServletRequest request )
+    {
+        return ( RequestInfo ) request.getAttribute( ServicesServlet.class.getName() );
+    }
+
+    private ServiceRegistration configurationPrinter;
+
+    public static final String LABEL = "services";
+
+    public static final String TITLE = "Services";
+
+
+    public void activate( BundleContext bundleContext )
+    {
+        super.activate( bundleContext );
+        configurationPrinter = bundleContext.registerService( ConfigurationPrinter.SERVICE, this, null );
+    }
+
+
+    public void deactivate()
+    {
+        if ( configurationPrinter != null )
+        {
+            configurationPrinter.unregister();
+            configurationPrinter = null;
+        }
+
+        super.deactivate();
+    }
+
+
+    public String getLabel()
+    {
+        return LABEL;
+    }
+
+
+    public String getTitle()
+    {
+        return TITLE;
+    }
+
+
+    public void printConfiguration( PrintWriter pw )
+    {
+        try
+        {
+            StringWriter w = new StringWriter();
+            writeJSON( w, null, true );
+            String jsonString = w.toString();
+            JSONObject json = new JSONObject( jsonString );
+
+            pw.println( "Status: " + json.get( "status" ) );
+            pw.println();
+
+            JSONArray data = json.getJSONArray( "data" );
+            for ( int i = 0; i < data.length(); i++ )
+            {
+                if ( !data.isNull( i ) )
+                {
+                    JSONObject service = data.getJSONObject( i );
+
+                    pw.println( MessageFormat.format( "Service {0} - {1} (pid: {2})", new Object[]
+                        { service.get( "id" ), service.get( "types" ), service.get( "pid" ) } ) );
+                    pw.println( MessageFormat.format( "  from Bundle {0} - {1} ({2}), version {3}", new Object[]
+                        { service.get( "bundleId" ), service.get( "bundleName" ), service.get( "bundleSymbolicName" ),
+                            service.get( "bundleVersion" ) } ) );
+
+                    JSONArray props = service.getJSONArray( "props" );
+                    for ( int pi = 0; pi < props.length(); pi++ )
+                    {
+                        if ( !props.isNull( pi ) )
+                        {
+                            JSONObject entry = props.getJSONObject( pi );
+
+                            pw.print( "    " + entry.get( "key" ) + ": " );
+
+                            Object entryValue = entry.get( "value" );
+                            if ( entryValue instanceof JSONArray )
+                            {
+                                pw.println();
+                                JSONArray entryArray = ( JSONArray ) entryValue;
+                                for ( int ei = 0; ei < entryArray.length(); ei++ )
+                                {
+                                    if ( !entryArray.isNull( ei ) )
+                                    {
+                                        pw.println( "        " + entryArray.get( ei ) );
+                                    }
+                                }
+                            }
+                            else
+                            {
+                                pw.println( entryValue );
+                            }
+                        }
+                    }
+
+                    JSONArray usingBundles = service.getJSONArray( "usingBundles" );
+                    for ( int ui = 0; ui < usingBundles.length(); ui++ )
+                    {
+                        if ( !usingBundles.isNull( ui ) )
+                        {
+                            JSONObject bundle = usingBundles.getJSONObject( ui );
+                            pw.println( MessageFormat.format( "  Using Bundle {0} - {1} ({2}), version {3}", new Object[]
+                                { bundle.get( "bundleId" ), bundle.get( "bundleName" ),
+                                    bundle.get( "bundleSymbolicName" ), bundle.get( "bundleVersion" ) } ) );
+                        }
+                    }
+
+                    pw.println();
+                }
+            }
+        }
+        catch ( Exception e )
+        {
+            getLog().log( LogService.LOG_ERROR, "Problem rendering Bundle details for configuration status", e );
+        }
+    }
+
+
+    private void appendServiceInfoCount( final StringBuffer buf, String msg, int count )
+    {
+        buf.append( count );
+        buf.append( " service" );
+        if ( count != 1 )
+            buf.append( 's' );
+        buf.append( ' ' );
+        buf.append( msg );
+    }
+
+
+    private ServiceReference getServiceById( String pathInfo )
+    {
+        // only use last part of the pathInfo
+        pathInfo = pathInfo.substring( pathInfo.lastIndexOf( '/' ) + 1 );
+
+        StringBuffer filter = new StringBuffer();
+        filter.append( "(" ).append( Constants.SERVICE_ID ).append( "=" );
+        filter.append( pathInfo ).append( ")" );
+        String filterStr = filter.toString();
+        try
+        {
+            ServiceReference[] refs = getBundleContext().getServiceReferences( null, filterStr );
+            if ( refs == null || refs.length != 1 )
+            {
+                return null;
+            }
+            return refs[0];
+        }
+        catch ( InvalidSyntaxException e )
+        {
+            getLog().log( LogService.LOG_WARNING, "Unable to search for services using filter " + filterStr, e );
+            // this shouldn't happen
+            return null;
+        }
+    }
+
+
+    private ServiceReference[] getServices()
+    {
+        try
+        {
+            return getBundleContext().getServiceReferences( null, null );
+        }
+        catch ( InvalidSyntaxException e )
+        {
+            getLog().log( LogService.LOG_WARNING, "Unable to access service reference list.", e );
+            return new ServiceReference[0];
+        }
+    }
+
+
+    private String getStatusLine( final ServiceReference[] services )
+    {
+        final StringBuffer buffer = new StringBuffer();
+        buffer.append( "Services information: " );
+        appendServiceInfoCount( buffer, "in total", services.length );
+        return buffer.toString();
+    }
+
+
+    private void keyVal( JSONWriter jw, String key, Object value ) throws JSONException
+    {
+        if ( key != null && value != null )
+        {
+            jw.object();
+            jw.key( "key" );
+            jw.value( key );
+            jw.key( "value" );
+            jw.value( value );
+            jw.endObject();
+        }
+    }
+
+
+    private String propertyAsString( ServiceReference ref, String name )
+    {
+        Object value = ref.getProperty( name );
+        if ( value instanceof Object[] )
+        {
+            StringBuffer dest = new StringBuffer();
+            Object[] values = ( Object[] ) value;
+            for ( int j = 0; j < values.length; j++ )
+            {
+                if ( j > 0 )
+                    dest.append( ", " );
+                dest.append( values[j] );
+            }
+            return dest.toString();
+        }
+        else if ( value != null )
+        {
+            return value.toString();
+        }
+        else
+        {
+            return "n/a";
+        }
+    }
+
+
+    private void renderJSON( final HttpServletResponse response, final ServiceReference service )
+        throws IOException
+    {
+        response.setContentType( "application/json" );
+        response.setCharacterEncoding( "UTF-8" );
+
+        final PrintWriter pw = response.getWriter();
+        writeJSON( pw, service );
+    }
+
+
+    private void serviceDetails( JSONWriter jw, ServiceReference service ) throws JSONException
+    {
+        String[] keys = service.getPropertyKeys();
+
+        jw.key( "props" );
+        jw.array();
+
+        for ( int i = 0; i < keys.length; i++ )
+        {
+            String key = keys[i];
+            if ( Constants.SERVICE_PID.equals( key ) )
+            {
+                keyVal( jw, "Service PID", service.getProperty( key ) );
+            }
+            else if ( Constants.SERVICE_DESCRIPTION.equals( key ) )
+            {
+                keyVal( jw, "Service Description", service.getProperty( key ) );
+            }
+            else if ( Constants.SERVICE_VENDOR.equals( key ) )
+            {
+                keyVal( jw, "Service Vendor", service.getProperty( key ) );
+            }
+            else if ( !Constants.OBJECTCLASS.equals( key ) && !Constants.SERVICE_ID.equals( key ) )
+            {
+                keyVal( jw, key, service.getProperty( key ) );
+            }
+
+        }
+
+        jw.endArray();
+
+    }
+
+
+    private void usingBundles( JSONWriter jw, ServiceReference service ) throws JSONException
+    {
+        jw.key( "usingBundles" );
+        jw.array();
+
+        Bundle[] usingBundles = service.getUsingBundles();
+        if ( usingBundles != null )
+        {
+            for ( int i = 0; i < usingBundles.length; i++ )
+            {
+                jw.object();
+                bundleInfo( jw, usingBundles[i] );
+                jw.endObject();
+            }
+        }
+
+        jw.endArray();
+
+    }
+
+
+    private void serviceInfo( JSONWriter jw, ServiceReference service, boolean details ) throws JSONException
+    {
+        jw.object();
+        jw.key( "id" );
+        jw.value( propertyAsString( service, Constants.SERVICE_ID ) );
+        jw.key( "types" );
+        jw.value( propertyAsString( service, Constants.OBJECTCLASS ) );
+        jw.key( "pid" );
+        jw.value( propertyAsString( service, Constants.SERVICE_PID ) );
+
+        bundleInfo( jw, service.getBundle() );
+
+        if ( details )
+        {
+            serviceDetails( jw, service );
+            usingBundles( jw, service );
+        }
+
+        jw.endObject();
+    }
+
+
+    private void bundleInfo( final JSONWriter jw, final Bundle bundle ) throws JSONException
+    {
+        jw.key( "bundleId" );
+        jw.value( bundle.getBundleId() );
+        jw.key( "bundleName" );
+        jw.value( Util.getName( bundle ) );
+        jw.key( "bundleVersion" );
+        jw.value( Util.getHeaderValue( bundle, Constants.BUNDLE_VERSION ) );
+        jw.key( "bundleSymbolicName" );
+        jw.value( Util.getHeaderValue( bundle, Constants.BUNDLE_SYMBOLICNAME ) );
+    }
+
+
+    private void writeJSON( final PrintWriter pw, final ServiceReference service ) throws IOException
+    {
+        writeJSON( pw, service, false );
+    }
+
+
+    private void writeJSON( final Writer pw, final ServiceReference service, final boolean fullDetails )
+        throws IOException
+    {
+        final ServiceReference[] allServices = this.getServices();
+        final String statusLine = this.getStatusLine( allServices );
+        final ServiceReference[] services = ( service != null ) ? new ServiceReference[]
+            { service } : allServices;
+
+        final JSONWriter jw = new JSONWriter( pw );
+
+        try
+        {
+            jw.object();
+
+            jw.key( "status" );
+            jw.value( statusLine );
+
+            jw.key( "data" );
+
+            jw.array();
+
+            for ( int i = 0; i < services.length; i++ )
+            {
+                serviceInfo( jw, services[i], fullDetails || service != null );
+            }
+
+            jw.endArray();
+
+            jw.endObject();
+
+        }
+        catch ( JSONException je )
+        {
+            throw new IOException( je.toString() );
+        }
+
+    }
+
+
+    protected void doGet( HttpServletRequest request, HttpServletResponse response ) throws ServletException,
+        IOException
+    {
+        final RequestInfo reqInfo = new RequestInfo( request );
+        if ( reqInfo.service == null && reqInfo.serviceRequested )
+        {
+            response.sendError( 404 );
+            return;
+        }
+        if ( reqInfo.extension.equals( "json" ) )
+        {
+            this.renderJSON( response, reqInfo.service );
+
+            // nothing more to do
+            return;
+        }
+
+        super.doGet( request, response );
+    }
+
+
+    protected void renderContent( HttpServletRequest request, HttpServletResponse response ) throws ServletException,
+        IOException
+    {
+        // get request info from request attribute
+        final RequestInfo reqInfo = getRequestInfo( request );
+        final PrintWriter pw = response.getWriter();
+
+        final String appRoot = ( String ) request.getAttribute( WebConsoleConstants.ATTR_APP_ROOT );
+
+        Util.startScript( pw );
+        pw.println( "var imgRoot = '" + appRoot + "/res/imgs';" );
+        pw.println( "var bundlePath = '" + appRoot + "/" + BundlesServlet.NAME + "/" + "';" );
+        pw.println( "var drawDetails = " + reqInfo.serviceRequested + ";" );
+        Util.endScript( pw );
+
+        Util.script( pw, appRoot, "services.js" );
+
+        pw.println( "<div id='plugin_content'/>" );
+        Util.startScript( pw );
+        pw.print( "renderServices(" );
+        writeJSON( pw, reqInfo.service );
+        pw.println( ");" );
+        Util.endScript( pw );
+
+    }
+}
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/misc/ConfigurationRender.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/misc/ConfigurationRender.java
index 5da56b6..b731941 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/misc/ConfigurationRender.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/misc/ConfigurationRender.java
@@ -24,10 +24,8 @@
 import java.text.DateFormat;
 import java.text.MessageFormat;
 import java.text.SimpleDateFormat;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Date;
-import java.util.Dictionary;
 import java.util.Iterator;
 import java.util.Locale;
 import java.util.Properties;
@@ -46,10 +44,6 @@
 import org.apache.felix.webconsole.WebConsoleConstants;
 import org.apache.felix.webconsole.internal.BaseWebConsolePlugin;
 import org.apache.felix.webconsole.internal.Util;
-import org.osgi.framework.Bundle;
-import org.osgi.framework.Constants;
-import org.osgi.framework.InvalidSyntaxException;
-import org.osgi.framework.ServiceReference;
 import org.osgi.util.tracker.ServiceTracker;
 
 
@@ -189,7 +183,6 @@
     private void printConfigurationStatus( ConfigurationWriter pw )
     {
         this.printSystemProperties( pw );
-        this.printServices( pw );
         this.printThreads( pw );
 
         for ( Iterator cpi = getConfigurationPrinters().iterator(); cpi.hasNext(); )
@@ -277,60 +270,6 @@
     //    }
 
 
-    private void printServices( ConfigurationWriter pw )
-    {
-        pw.title(  "Services" );
-
-        // get the list of services sorted by service ID (ascending)
-        SortedMap srMap = new TreeMap();
-        try
-        {
-            ServiceReference[] srs = getBundleContext().getAllServiceReferences( null, null );
-            for ( int i = 0; i < srs.length; i++ )
-            {
-                srMap.put( srs[i].getProperty( Constants.SERVICE_ID ), srs[i] );
-            }
-        }
-        catch ( InvalidSyntaxException ise )
-        {
-            // should handle, for now just print nothing, actually this is not
-            // expected
-        }
-
-        for ( Iterator si = srMap.values().iterator(); si.hasNext(); )
-        {
-            ServiceReference sr = ( ServiceReference ) si.next();
-
-            infoLine( pw, null, String.valueOf( sr.getProperty( Constants.SERVICE_ID ) ), sr
-                .getProperty( Constants.OBJECTCLASS ) );
-            infoLine( pw, "  ", "Bundle", this.getBundleString( sr.getBundle() ) );
-
-            Bundle[] users = sr.getUsingBundles();
-            if ( users != null && users.length > 0 )
-            {
-                for ( int i = 0; i < users.length; i++ )
-                {
-                    infoLine( pw, "  ", "Using Bundle", this.getBundleString( users[i] ) );
-                }
-            }
-
-            String[] keys = sr.getPropertyKeys();
-            Arrays.sort( keys );
-            for ( int i = 0; i < keys.length; i++ )
-            {
-                if ( !Constants.SERVICE_ID.equals( keys[i] ) && !Constants.OBJECTCLASS.equals( keys[i] ) )
-                {
-                    infoLine( pw, "  ", keys[i], sr.getProperty( keys[i] ) );
-                }
-            }
-
-            pw.println();
-        }
-
-        pw.end();
-    }
-
-
     private void printConfigurationPrinter( ConfigurationWriter pw, ConfigurationPrinter cp )
     {
         pw.title(  cp.getTitle() );
@@ -349,79 +288,37 @@
         if ( label != null )
         {
             pw.print( label );
-            pw.print( '=' );
+            pw.print( " = " );
         }
 
-        printObject( pw, value );
+        pw.print( asString( value ) );
 
         pw.println();
     }
 
 
-    private static void printObject( PrintWriter pw, Object value )
+    private static String asString( final Object value )
     {
         if ( value == null )
         {
-            pw.print( "null" );
+            return "n/a";
         }
         else if ( value.getClass().isArray() )
         {
-            printArray( pw, ( Object[] ) value );
-        }
-        else
-        {
-            pw.print( value );
-        }
-    }
-
-
-    private static void printArray( PrintWriter pw, Object[] values )
-    {
-        pw.print( '[' );
-        if ( values != null && values.length > 0 )
-        {
-            for ( int i = 0; i < values.length; i++ )
+            StringBuffer dest = new StringBuffer();
+            Object[] values = ( Object[] ) value;
+            for ( int j = 0; j < values.length; j++ )
             {
-                if ( i > 0 )
-                {
-                    pw.print( ", " );
-                }
-                printObject( pw, values[i] );
+                if ( j > 0 )
+                    dest.append( ", " );
+                dest.append( values[j] );
             }
-        }
-        pw.print( ']' );
-    }
-
-
-    private String getBundleString( Bundle bundle )
-    {
-        StringBuffer buf = new StringBuffer();
-
-        if ( bundle.getSymbolicName() != null )
-        {
-            buf.append( bundle.getSymbolicName() );
-        }
-        else if ( bundle.getLocation() != null )
-        {
-            buf.append( bundle.getLocation() );
+            return dest.toString();
         }
         else
         {
-            buf.append( bundle.getBundleId() );
+            return value.toString();
         }
-
-        Dictionary headers = bundle.getHeaders();
-        if ( headers.get( Constants.BUNDLE_VERSION ) != null )
-        {
-            buf.append( " (" ).append( headers.get( Constants.BUNDLE_VERSION ) ).append( ')' );
-        }
-
-        if ( headers.get( Constants.BUNDLE_NAME ) != null )
-        {
-            buf.append( " \"" ).append( headers.get( Constants.BUNDLE_NAME ) ).append( '"' );
-        }
-
-        return buf.toString();
     }
 
 
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 cc32455..cdb3883 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
@@ -103,6 +103,7 @@
             "org.apache.felix.webconsole.internal.compendium.LogServlet",
             "org.apache.felix.webconsole.internal.compendium.PreferencesConfigurationPrinter",
             "org.apache.felix.webconsole.internal.core.BundlesServlet",
+            "org.apache.felix.webconsole.internal.core.ServicesServlet",
             "org.apache.felix.webconsole.internal.core.InstallAction",
             "org.apache.felix.webconsole.internal.core.SetStartLevelAction",
             "org.apache.felix.webconsole.internal.deppack.DepPackServlet",
diff --git a/webconsole/src/main/resources/res/ui/services.js b/webconsole/src/main/resources/res/ui/services.js
new file mode 100644
index 0000000..29691e5
--- /dev/null
+++ b/webconsole/src/main/resources/res/ui/services.js
@@ -0,0 +1,199 @@
+/*
+ * 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.
+ */
+function renderStatusLine() {
+	$("#plugin_content").append(
+			"<div class='fullwidth'><div class='statusline'/></div>");
+}
+
+function renderView( /* Array of String */columns) {
+	renderStatusLine();
+	var txt = "<div class='table'><table id='plugin_table' class='tablelayout'><thead><tr>";
+	for ( var name in columns) {
+		txt = txt + "<th class='col_" + columns[name] + "'>" + columns[name]
+				+ "</th>";
+	}
+	txt = txt + "</tr></thead><tbody></tbody></table></div>";
+	$("#plugin_content").append(txt);
+	renderStatusLine();
+}
+
+function renderData(eventData) {
+	$(".statusline").empty().append(eventData.status);
+	$("#plugin_table > tbody > tr").remove();
+	for ( var idx in eventData.data) {
+		entry(eventData.data[idx]);
+	}
+	$("#plugin_table").trigger("update");
+	if (drawDetails) {
+		renderDetails(eventData);
+	}
+}
+
+function entry( /* Object */dataEntry) {
+	var trElement = tr(null, {
+		id : "entry" + dataEntry.id
+	});
+	entryInternal(trElement, dataEntry);
+	$("#plugin_table > tbody").append(trElement);
+}
+
+function entryInternal( /* Element */parent, /* Object */dataEntry) {
+	var id = dataEntry.id;
+	var name = dataEntry.id;
+
+	var inputElement = createElement("img", "rightButton", {
+		src : appRoot + "/res/imgs/arrow_right.png",
+		style : {
+			border : "none"
+		},
+		id : 'img' + id,
+		title : "Details",
+		alt : "Details",
+		width : 14,
+		height : 14
+	});
+	$(inputElement).click(function() {
+		showDetails(id)
+	});
+	var titleElement;
+	if (drawDetails) {
+		titleElement = text(name);
+	} else {
+		titleElement = createElement("a", null, {
+			href : window.location.pathname + "/" + id
+		});
+		titleElement.appendChild(text(name));
+	}
+	var bundleElement = createElement("a", null, {
+		href : bundlePath + dataEntry.bundleId
+	});
+	bundleElement.appendChild(text(dataEntry.bundleSymbolicName + " ("
+			+ dataEntry.bundleId + ")"));
+
+	parent
+			.appendChild(td(null, null,
+					[ inputElement, text(" "), titleElement ]));
+	parent.appendChild(td(null, null, [ text(dataEntry.types) ]));
+	parent.appendChild(td(null, null, [ bundleElement ]));
+}
+
+function showDetails(id) {
+	$.get(pluginRoot + "/" + id + ".json", null, function(data) {
+		renderDetails(data);
+	}, "json");
+}
+
+function hideDetails(id) {
+	$("#img" + id).each(function() {
+		$("#pluginInlineDetails").remove();
+		$(this).attr("src", appRoot + "/res/imgs/arrow_right.png");
+		$(this).attr("title", "Details");
+		$(this).attr("alt", "Details");
+		$(this).unbind('click').click(function() {
+			showDetails(id)
+		});
+	});
+}
+
+function renderDetails(data) {
+	data = data.data[0];
+	$("#pluginInlineDetails").remove();
+	$("#entry" + data.id + " > td").eq(1).append(
+			"<div id='pluginInlineDetails'/>");
+	$("#img" + data.id).each(function() {
+		if (drawDetails) {
+			$(this).attr("src", appRoot + "/res/imgs/arrow_left.png");
+			$(this).attr("title", "Back");
+			$(this).attr("alt", "Back");
+			var ref = window.location.pathname;
+			ref = ref.substring(0, ref.lastIndexOf('/'));
+			$(this).unbind('click').click(function() {
+				window.location = ref;
+			});
+		} else {
+			$(this).attr("src", appRoot + "/res/imgs/arrow_down.png");
+			$(this).attr("title", "Hide Details");
+			$(this).attr("alt", "Hide Details");
+			$(this).unbind('click').click(function() {
+				hideDetails(data.id)
+			});
+		}
+	});
+	$("#pluginInlineDetails").append(
+			"<table border='0'><tbody></tbody></table>");
+	var details = data.props;
+	for ( var idx in details) {
+		var prop = details[idx];
+
+		var txt = "<tr><td class='aligntop' noWrap='true' style='border:0px none'>"
+				+ prop.key
+				+ "</td><td class='aligntop' style='border:0px none'>";
+		if (prop.value) {
+			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 style='color: grey;'>!!"
+								+ value.substring(5) + "</span>";
+					} else if (value.substring(0, 7) == "ERROR: ") {
+						txt = txt + "<span style='color: red;'>!!"
+								+ value.substring(6) + "</span>";
+					} else {
+						txt = txt + value;
+					}
+					i++;
+				}
+			} else {
+				txt = txt + prop.value;
+			}
+		} else {
+			txt = txt + "\u00a0";
+		}
+		txt = txt + "</td></tr>";
+		$("#pluginInlineDetails > table > tbody").append(txt);
+
+	}
+}
+
+function renderServices(data) {
+	$(document).ready(function() {
+		renderView( [ "Id", "Type(s)", "Bundle" ]);
+		renderData(data);
+
+		var extractMethod = function(node) {
+			var link = node.getElementsByTagName("a");
+			if (link && link.length == 1) {
+				return link[0].innerHTML;
+			}
+			return node.innerHTML;
+		};
+		$("#plugin_table").tablesorter( {
+			headers : {
+				0 : {
+					sorter : "digit"
+				}
+			},
+			sortList : [ [ 1, 0 ] ],
+			textExtraction : extractMethod
+		});
+	});
+}
\ No newline at end of file