[FELIX-4162] incorrect loop detection, [FELIX-3769] webconsole categories, [FELIX-4163] command line interface

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1499958 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/webconsole-plugins/servicediagnostics/changelog.txt b/webconsole-plugins/servicediagnostics/changelog.txt
index 566f217..db86269 100644
--- a/webconsole-plugins/servicediagnostics/changelog.txt
+++ b/webconsole-plugins/servicediagnostics/changelog.txt
@@ -3,9 +3,15 @@
 
 ** Bug
   * [FELIX-3898] name parsing issue on BundleDependency
+  * [FELIX-4162] incorrect loop detection
 
 ** Improvement
   * [FELIX-3899] switch to Scala 2.10
+  * [FELIX-3769] webconsole categories
+  * [FELIX-4163] command line interface
+  * circular dependencies shown on a separate graph with or wihtout optionals
+  * show both using bundles and providing bundles
+  * show components as implementations ans services as interfaces
 
 Changes from 0.1.1 to 0.1.2
 ---------------------------
diff --git a/webconsole-plugins/servicediagnostics/core/pom.xml b/webconsole-plugins/servicediagnostics/core/pom.xml
index 7e24466..012e8ed 100644
--- a/webconsole-plugins/servicediagnostics/core/pom.xml
+++ b/webconsole-plugins/servicediagnostics/core/pom.xml
@@ -27,7 +27,17 @@
     <dependency>
       <groupId>org.apache.felix</groupId>
       <artifactId>org.apache.felix.webconsole</artifactId>
-      <version>4.0.0</version>
+      <version>4.2.0</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.felix</groupId>
+      <artifactId>org.apache.felix.shell</artifactId>
+      <version>1.4.3</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.felix</groupId>
+      <artifactId>org.apache.felix.gogo.command</artifactId>
+      <version>0.10.0</version>
     </dependency>
     <dependency>
       <groupId>org.apache.felix</groupId>
@@ -68,7 +78,8 @@
             </Import-Package>
             <Private-Package>
               org.apache.felix.servicediagnostics.impl,
-              org.apache.felix.servicediagnostics.webconsole
+              org.apache.felix.servicediagnostics.webconsole,
+              org.apache.felix.servicediagnostics.shell,
             </Private-Package>
             <Include-Resource>
               {maven-resources}
diff --git a/webconsole-plugins/servicediagnostics/core/src/main/resources/html/index.html b/webconsole-plugins/servicediagnostics/core/src/main/resources/html/index.html
index 893d481..68af2f6 100644
--- a/webconsole-plugins/servicediagnostics/core/src/main/resources/html/index.html
+++ b/webconsole-plugins/servicediagnostics/core/src/main/resources/html/index.html
@@ -21,26 +21,33 @@
     <script type="text/javascript">
 <!--
 
-var _redraw;
-var height = 600;
-var width = 1000;
+var _redraw
+var height = 600
+var width = 1000
 
-DEBUG = false;
-debug = function (obj) { if(DEBUG && console) console.debug(obj); }
+DEBUG = false
+debug = function (obj) { if(DEBUG && console) console.debug(obj) }
 
-var services;
+function isEmpty(obj) { 
+  for (var key in obj) return false 
+  return true
+}
+
+var services
+var grapher
 
 // from graph demo @ http://blog.ameisenbar.de/en/2010/03/02/dracula/
 function graphUnavail(json) {
-  var g = new Graph();
+  $("#legend").html("Bubbles are components, dotted squares are missing required dependencies.")
+  var g = new Graph()
 
-  var empty = true;
-  notavail = json.notavail;
+  var empty = true
+  notavail = json.notavail
   for (s in notavail) {
-    empty = false;
+    empty = false
     for (i = 0; i < notavail[s].length; i++) {
       // point unregistered service to dependency name
-      var dep = notavail[s][i];
+      var dep = notavail[s][i]
       g.addNode(dep, {
         getShape : function(r,x,y) {
           // create a dashed square shape to differentiate the missing dependency
@@ -49,107 +56,149 @@
             "stroke": "gray", 
             "stroke-width": 2, 
             "stroke-dasharray": "--"
-          });
+          })
         }
-      });
-      g.addEdge(s, dep, { directed : true } );
+      })
+      g.addEdge(s, dep, { directed : true } )
     }
   }
-  // show unresolved
-  var unresolved = json.unresolved
-  if (unresolved) for (s in unresolved) {
-    $("#warning").html("circular dependencies detected!");
-    for (i = 0; i < unresolved[s].length; i++) {
-      g.addEdge(s, unresolved[s][i], { directed : true } );
-    }
-  }
+  // warn unresolved
+  if (json.unresolved && !isEmpty(json.unresolved)) 
+    $("#warning").html("circular dependencies detected! <a href='javascript:graphUnresolved()'>(show)</a>")
+  else 
+    $("#warning").html("") //clear previous
 
   if (empty) {
-    $("#canvas").empty().append($("<h1>").html("Service Registry status OK: No unresolved service found."));
+    $("#canvas").empty().append($("<h1>").html("Service Registry status OK: No unresolved service found."))
   } 
   else showGraph(g)
 }
 
-function graphAllServices(json) {
-  var g = new Graph();
+function graphUnresolved() {
+  grapher = graphUnresolved
+  $("#legend").html("Bubbles are unresolvable components linked to each other.")
+  var g = new Graph()
+  var unresolved = services.unresolved
+  for (s in unresolved) {
+    for (i = 0; i < unresolved[s].length; i++) {
+      g.addEdge(s, unresolved[s][i], { directed : true } )
+    }
+  }
+  showGraph(g)
+}
 
-  var empty = true;
+function graphUsingServices(json) {
+  $("#legend").html("Black squares are bundles, pointing to the services they use.")
+  var g = new Graph()
+
+  var empty = true
   for (s in json) {
-    empty = false;
+    empty = false
     for (i = 0; i < json[s].length; i++) {
       // point using bundle to service name
-      var bundle = json[s][i];
+      var bundle = json[s][i]
       g.addNode(bundle, {
         getShape : function(r,x,y) {
           // create a square shape to differentiate bundles from services
-          return r.rect(x-30, y-13, 62, 33, 5).attr({"fill": "#f00", "stroke-width": 2});
+          return r.rect(x-30, y-13, 62, 33, 5).attr({"fill": "#f00", "stroke-width": 2})
         }
-      });
-      g.addEdge(bundle, s, { directed : true } );
+      })
+      g.addEdge(bundle, s, { directed : true } )
     }
   }
 
   if (empty) {
-    $("#canvas").empty().append($("<h1>").html("Service Registry empty: no service found."));
+    $("#canvas").empty().append($("<h1>").html("Service Registry empty: no service found."))
+  }
+  else showGraph(g)
+}
+
+function graphServiceProviders(json) {
+  $("#legend").html("Black squares are bundles, pointing to the services they provide.")
+  var g = new Graph()
+
+  var empty = true
+  for (bundle in json) {
+    empty = false
+    g.addNode(bundle, {
+      getShape : function(r,x,y) {
+        // create a square shape to differentiate bundles from services
+        return r.rect(x-30, y-13, 62, 33, 5).attr({"fill": "#f00", "stroke-width": 2})
+      }
+    })
+    for (i = 0; i < json[bundle].length; i++) {
+      // point bundle to service name
+      var service = json[bundle][i]
+      g.addEdge(bundle, service, { directed : true } )
+    }
+  }
+
+  if (empty) {
+    $("#canvas").empty().append($("<h1>").html("Service Registry empty: no service found."))
   }
   else showGraph(g)
 }
 
 function showGraph(g) {
-    debug(g);
+    debug(g)
+    $("#warning").html("")
 
     /* layout the graph using the Spring layout implementation */
-    var layouter = new Graph.Layout.Spring(g);
-    layouter.layout();
+    var layouter = new Graph.Layout.Spring(g)
+    layouter.layout()
     
     /* draw the graph using the RaphaelJS draw implementation */
-    $("#canvas").empty();
-    var renderer = new Graph.Renderer.Raphael('canvas', g, width, height);
-    renderer.draw();
+    $("#canvas").empty()
+    var renderer = new Graph.Renderer.Raphael('canvas', g, width, height)
+    renderer.draw()
     
     _redraw = function() {
-        layouter.layout();
-        renderer.draw();
-    };
+        layouter.layout()
+        renderer.draw()
+    }
+    $("#filterdiv").show()
 }
 
 function redraw() {
-  var filter = $("#filter").val();
+  var filter = $("#filter").val()
   if (filter) {
-    var grep = {}; 
-    for (s in services) if (s.indexOf(filter) >= 0) grep[s] = services[s]; 
-    graphAllServices(grep);
+    var grep = {}
+    for (s in services) if (s.indexOf(filter) >= 0) grep[s] = services[s]
+    grapher(grep)
   } else {
-    graphAllServices(services);
+    grapher(services)
   }
 }
 
 function loadUnavail() {
-  $("#canvas").html("Loading data. Please wait...");
-  $.ajax({
-    url: "servicegraph/notavail",
-    dataType: "json",
-    success: function(json){
-      services = json;
-      debug("Got services");
-      debug(json);
-      graphUnavail(json);
-    }
-  });
+  var withOpt = ""
+  if ($("#optionals").attr("checked")) withOpt = "?optionals=true"
+  grapher = graphUnavail
+  loadServices("notavail"+withOpt)
 }
 
-function loadAllServices() {
-  $("#canvas").html("Loading data. Please wait...");
+function loadServiceProviders() {
+  grapher = graphServiceProviders
+  loadServices("providing")
+}
+
+function loadServiceUsers() {
+  grapher = graphUsingServices
+  loadServices("using")
+}
+
+function loadServices(cmd) {
+  $("#canvas").html("Loading data. Please wait...")
   $.ajax({
-    url: "servicegraph/all",
+    url: "servicegraph/"+cmd,
     dataType: "json",
     success: function(json){
-      services = json;
-      debug("Got services");
-      debug(json);
-      graphAllServices(json);
+      services = json
+      debug("Got services")
+      debug(json)
+      grapher(json)
     }
-  });
+  })
 }
 
 /* only do all this when document has finished loading (needed for RaphaelJS) */
@@ -158,20 +207,26 @@
     $.getScript("servicegraph/html/js/graffle-1.3.1.js")
     $.getScript("servicegraph/html/js/graph.js")
     $("#actions")
-  	.append($("<a>").attr("href", "javascript:loadAllServices()").html("Show Service Registry"))
+  	.append($("<a>").attr("href", "javascript:loadServiceProviders()").html("Show Service Providers"))
+  	.append($("<span>").html("&nbsp;|&nbsp;"))
+  	.append($("<a>").attr("href", "javascript:loadServiceUsers()").html("Show Service Users"))
   	.append($("<span>").html("&nbsp;|&nbsp;"))
   	.append($("<a>").attr("href", "javascript:loadUnavail()").html("Show Not Avail"))
-});
+  	.append($("<span>").html("&nbsp;"))
+        .append($("<input>").attr("id", "optionals").attr("type", "checkbox"))
+        .append($("<span>").html("Include optionals in loops"))
+})
 
 -->
     </script>
     <style>
       #actions a { color:black; font-weight:bold; text-decoration:none; }
       #warning { color:red; font-weight:bold; }
+      #filterdiv { visibility:none; display:none; }
     </style>
 <div id="servicegraph">
-Filter:&nbsp;<input type="text" id="filter"/><button id="redraw" onclick="redraw();">redraw</button>
-&nbsp;&nbsp;<span id="actions"></span>
-&nbsp;&nbsp;<span id="warning"></span>
+<span id="actions"></span>&nbsp;&nbsp;<span id="warning"></span>
+<div id="filterdiv">Filter:&nbsp;<input type="text" id="filter"/><button id="redraw" onclick="redraw();">redraw</button></div>
+<div><span id="legend"></span>&nbsp;<span>(All nodes can be dragged around)</span></div>
 <div id="canvas"></div>
 </div>
diff --git a/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/ServiceDiagnostics.scala b/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/ServiceDiagnostics.scala
index 71a320a..77e5756 100644
--- a/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/ServiceDiagnostics.scala
+++ b/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/ServiceDiagnostics.scala
@@ -32,12 +32,18 @@
 
     /**
      * returns a graph of unresolvable components (typically loops)
+     * @param optionals if true, include optional services in loop detection
      */
-    def unresolved :Map[String, List[String]]
+    def unresolved(optionals:Boolean) :Map[String, List[String]]
 
     /**
      * returns a map of resolved service names to list of bundles using the service
      */
-    def allServices:Map[String, List[String]]
+    def usingBundles:Map[String, List[String]]
+
+    /**
+     * returns a map of bundle names to list of provided services
+     */
+    def serviceProviders:Map[String, List[String]]
 }
 
diff --git a/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/ServiceDiagnosticsPlugin.scala b/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/ServiceDiagnosticsPlugin.scala
index 4fb847a..1ae85e9 100644
--- a/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/ServiceDiagnosticsPlugin.scala
+++ b/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/ServiceDiagnosticsPlugin.scala
@@ -21,6 +21,10 @@
 import org.osgi.framework.FrameworkUtil
 import collection.JavaConversions._
 
+import scala.collection.mutable.{Map => mMap}
+
+import org.json.JSONObject
+
 import Util._
 
 /**
@@ -40,33 +44,40 @@
 
 /**
  * This class represents a service component.
- * @param name the service interface name 
+ * @param impl the implementation class name
+ * @param service the service interface name 
  *   (use different instances for objects registering multiple services)
  * @param props the service properties
  * @param registered true if the component is already registered in the Service Registry
  * @param deps the list of declared dependencies
  */
-class Comp(val name:String, val props:java.util.Dictionary[_,_], val registered:Boolean, val deps:List[Dependency])
+class Comp(val impl:String, val service:String, val props:java.util.Dictionary[_,_], val registered:Boolean, val deps:List[Dependency])
 {
-    override def toString = {if (registered) "[registered]" else "[unregistered]"}+shorten(name)+{
-        if (props != null && !props.isEmpty) " "+props else ""}
+    override def toString = {if (registered) "[registered]" else "[unregistered]"}+shorten(impl)+"("+shorten(service)+
+        Option(props).map("#"+_.toString.hashCode).mkString+")" //properties can be too long to display :(
+    
+    override def equals(o:Any) = o != null && o.getClass == getClass && {
+      val oc = o.asInstanceOf[Comp]
+      oc.impl == impl && oc.service == service && oc.props == props
+    }
 }
 
 /**
  * This class represents a service dependency.
  * @param name the service interface name
  * @param filter the optional service filter
- * @param available true if the dependency is already available in the Service Registry
+ * @param available true if the dependency is already available in the Service Registry, 
+ *         or optional (irrelevant for diagnostics)
  */
-class Dependency(val name:String, val filter:String, val available:Boolean = false) 
+class Dependency(val name:String, val filter:Option[String], val available:Boolean = false) 
 {
-    private val compiled = if (filter != null && !filter.isEmpty) FrameworkUtil.createFilter(filter) else null
+    private val compiled = filter.map(FrameworkUtil.createFilter(_))
 
-    def matchedBy(comp:Comp):Boolean = comp.name == name && 
-        !(compiled != null && comp.props == null) && //filter and no props, doesn't match
-        (compiled == null || compiled.`match`(comp.props))
+    def matchedBy(comp:Comp):Boolean = comp.service == name && 
+        !(compiled.isDefined && comp.props == null) && //filter and no props, doesn't match
+        (compiled.isEmpty || compiled.get.`match`(comp.props))
     
-    override def toString = shorten(name)+{if (filter != null) filter else ""}
+    override def toString = shorten(name) + filter.mkString
 }
 
 /** 
@@ -81,4 +92,10 @@
         val l = classname.split('.').toList
         l.map(_.take(1)).mkString(".") + l.last.drop(1)
     }
+
+    /** 
+     * turn the ServiceDiagnostics output into a JSON representation.
+     */
+    def json(map:Map[String,List[AnyRef]]) = 
+      new JSONObject(asJavaMap(mMap() ++ map.map(kv => (kv._1, asJavaList(kv._2)))))
 }
diff --git a/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/impl/Activator.scala b/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/impl/Activator.scala
index b5ec325..c88506a 100644
--- a/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/impl/Activator.scala
+++ b/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/impl/Activator.scala
@@ -22,12 +22,16 @@
 
 import org.osgi.framework.BundleContext
 
+import org.apache.felix.shell.Command
+import org.apache.felix.service.command.CommandProcessor
+
 import org.apache.felix.dm.DependencyActivatorBase
 import org.apache.felix.dm.DependencyManager
 
 import org.apache.felix.servicediagnostics.ServiceDiagnostics
 import org.apache.felix.servicediagnostics.ServiceDiagnosticsPlugin
 import org.apache.felix.servicediagnostics.webconsole.WebConsolePlugin
+import org.apache.felix.servicediagnostics.shell.CLI
 
 /**
  * Activator class for the service diagnostics core implementation
@@ -69,6 +73,18 @@
                 .setService(classOf[ServiceDiagnostics])
                 .setRequired(true)
                 .setAutoConfig("engine")))
+
+        // register the shell command
+        dm.add(createComponent
+            .setInterface(classOf[Command].getName, new jHT[String,Any]() {{
+                  put(CommandProcessor.COMMAND_FUNCTION, Array("notavail", "circular"))
+                  put(CommandProcessor.COMMAND_SCOPE, "sd")
+              }})
+            .setImplementation(classOf[CLI])
+            .add(createServiceDependency
+                .setService(classOf[ServiceDiagnostics])
+                .setRequired(true)
+                .setAutoConfig("engine")))
     }
 
     override def destroy(bc:BundleContext, dm:DependencyManager) = {}
diff --git a/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/impl/DMNotAvail.scala b/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/impl/DMNotAvail.scala
index 2985673..a9e87c1 100644
--- a/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/impl/DMNotAvail.scala
+++ b/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/impl/DMNotAvail.scala
@@ -43,20 +43,27 @@
     override def components:List[Comp] = 
     {
         // this involves a bit of type casting gymnastics because the underlying 
-        // API does not use generic types
+        // API does not use generic types and parsing of strings because the 
+        // underlying API does not provide accessors
         (for {
             dm <- DependencyManager.getDependencyManagers.map(_.asInstanceOf[DependencyManager])
             comp <- dm.getComponents.map(_.asInstanceOf[Component])
             compdec = comp.asInstanceOf[ComponentDeclaration]
+            impl = comp.toString.split(" ").toList.last.takeWhile(_ != ']').trim
+            service <- compdec.getName.takeWhile(_ != '(').split(",") //multiple services for one comp
             deps = compdec.getComponentDependencies
-                          .map(dep => new Dependency(dep.getName.takeWhile(_ != '('), 
-                                           dep.getName.dropWhile(_ != '(').trim,
+                          .map(dep => new Dependency(dep.getName.takeWhile(_ != '(').trim, 
+                                           dep.getName.dropWhile(_ != '(').trim match {
+                                               case "" => None
+                                               case f => Some(f)
+                                           },
                                            dep.getState != STATE_UNAVAILABLE_REQUIRED)).toList
           }
             // yield Comp builds a list of Comp out of the for comprehension
-            yield new Comp(compdec.getName.takeWhile(_ != '('), 
+            yield new Comp(impl,
+                           service.trim, 
                            comp.getServiceProperties,
                            (compdec.getState != STATE_UNREGISTERED),
-                           deps)) toList
+                           deps)) toList 
     }
 }
diff --git a/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/impl/DSNotAvail.scala b/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/impl/DSNotAvail.scala
index e1d3e9a..a8ce4f5 100644
--- a/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/impl/DSNotAvail.scala
+++ b/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/impl/DSNotAvail.scala
@@ -49,11 +49,12 @@
             service <- Option[Array[String]](comp.getServices).getOrElse(Array())
             deps = Option[Array[Reference]](comp.getReferences).getOrElse(Array())
                           .map(dep => new Dependency(dep.getServiceName,
-                                                     dep.getTarget,
-                                                     dep.isSatisfied)).toList
+                                                     Option(dep.getTarget),
+                                                     dep.isSatisfied || dep.isOptional)).toList
           }
             // yield Comp builds a list of Comp out of the for comprehension
-            yield new Comp(service,
+            yield new Comp(comp.getClassName.trim,
+                           service.trim,
                            comp.getProperties,
                            comp.getState != Component.STATE_UNSATISFIED,
                            deps)) toList
diff --git a/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/impl/ServiceDiagnosticsImpl.scala b/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/impl/ServiceDiagnosticsImpl.scala
index a2eaf6c..17903ce 100644
--- a/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/impl/ServiceDiagnosticsImpl.scala
+++ b/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/impl/ServiceDiagnosticsImpl.scala
@@ -20,12 +20,14 @@
 
 import scala.collection.mutable.Buffer
 import scala.collection.mutable.{Set => mSet}
+import scala.collection.JavaConversions._
 
 import org.osgi.framework.BundleContext
 import org.osgi.framework.ServiceReference
 import org.osgi.framework.Constants.OBJECTCLASS
 
 import org.apache.felix.servicediagnostics._
+import org.apache.felix.servicediagnostics.Util._
 
 /**
  * This is the ServiceDiagnostics implementation. 
@@ -41,30 +43,34 @@
     /**
      * Implements ServiceDiagnostics.notavail.
      * 
-     * This method gathers components information from all plugins
+     * This method aggregates unregistered components from all plugins
      * and filters all intermediate known unregistered services
      * to keep only missing "leaf" dependencies
      */
     override def notavail :Map[String, List[String]] = 
     {
-        val unavail :List[Comp] = for {
-                          plugin <- plugins.toList
-                          comp <- plugin.components
-                          if (! comp.registered)
-                      } yield comp
-        (for {
-            comp <- unavail
-            dep <- comp.deps.filterNot(_.available)
-            if (! unavail.exists(c => dep.matchedBy(c)))
-        } yield comp.toString -> comp.deps.filterNot(_.available).map(_.toString) ) toMap
-
+        val unavail = plugins.flatMap(_.components).filterNot(_.registered)
+        unavail.foldLeft(Map[String,List[String]]()) { (map,comp) =>
+            val missing = comp.deps.filterNot { d =>
+                  d.available || unavail.exists(c => d.matchedBy(c))
+                }.map(_.toString) 
+            if (missing isEmpty) map else map + (shorten(comp.impl) -> missing)
+        }
     }
     
     class Node(val comp:Comp, val edges:mSet[Node] = mSet[Node]()) {
-      def name = comp.toString
+      def name = comp.impl
       override def toString = name + " -> " + edges.map(_.name)
+      override def equals(o:Any) = o != null && o.getClass == getClass && o.asInstanceOf[Node].comp == comp
     }
 
+    //debug helper
+    def json(l:Iterable[Node]) = l.toList.foldLeft(new org.json.JSONArray()) { (j,n) => 
+      j.put(new org.json.JSONObject(new java.util.HashMap[String,java.util.List[String]] {{
+          put(n.name, new java.util.ArrayList[String] {{ addAll(n.edges.map(_.name)) }})
+        }}))
+    }.toString(2)
+
     /**
      * Implements ServiceDiagnostics.unresolved.
      * 
@@ -76,21 +82,26 @@
      * from the original graph. This is done because "perfect loops" have no border node and are 
      * therefore "invisible" to the traversing algorithm.
      */
-    override def unresolved :Map[String, List[String]] = 
+    override def unresolved(optionals:Boolean) :Map[String, List[String]] = 
     {
         // first build a traversable graph from all found components and dependencies
         def buildGraph(link:(Node,Node)=>Unit) = {
             // concatenate component nodes from all plugins
-            val allnodes = for ( p <- plugins; comp <- p.components ) yield new Node(comp)
+            val allnodes = plugins.flatMap(_.components).map(new Node(_))
 
             // and connect the nodes according to component dependencies
             // the 'link' method gives the direction of the link
-            for ( node <- allnodes; dep <- node.comp.deps )
+            // note that all dependencies not pointing to a known component are dropped from the graph
+            for {
+              node <- allnodes 
+              dep <- node.comp.deps 
+              if (optionals || !dep.available)
+            }
             {
                 allnodes.filter(n => dep.matchedBy(n.comp)).foreach(n => link(node, n) )
             }
 
-            allnodes.toList //return the graph
+            allnodes.toSet //return the graph
         }
 
         // a "forward" graph of who depends on who
@@ -121,44 +132,49 @@
         // now traverse the graph starting from border nodes (nodes not pointed by anyone)
         val resolved:Set[Node] = (for { 
             border <- triggers filter (_.edges.size == 0)
-            node <- graph.find(_.name == border.name)
+            node <- graph.find(_ == border) // graph and triggers contain different Node instances; this uses the overriden equals methods
         } yield resolve(node)).flatten.toSet
 
         // finally filter the original graph by removing all resolved nodes
         // and format the result (keeping only the names)
-        (for (node <- graph.filterNot(n => resolved.contains(n)))
-            yield (node.name -> node.edges.map(_.name).toList)).toMap
+        (for (node <- graph.filterNot(n => n.edges.isEmpty || resolved.contains(n)))
+          yield (node.name -> node.edges.map{ n => n.name }.toList)).toMap
     }
 
     /**
-     * Implements ServiceDiagnostics.allServices.
+     * Implements ServiceDiagnostics.usingBundles.
      */
-    override def allServices:Map[String,List[String]] = 
+    override def usingBundles:Map[String,List[String]] = 
+        allServices.foldLeft(Map[String,List[String]]()) { case (result, (name, ref)) =>
+            Option(ref.getUsingBundles).map { _.toList.map(_.toString) }.getOrElse(Nil) match {
+                case using @ h::t => result + (name -> using)
+                case Nil => result
+            }
+        }
+
+    /**
+     * Implements ServiceDiagnostics.serviceProviders.
+     */
+    override def serviceProviders:Map[String, List[String]] = 
+        allServices.foldLeft(Map[String,List[String]]()) { case (result, (name, ref)) =>
+            val b = ref.getBundle.toString
+            result.updated(b, name :: result.getOrElse(b, Nil))
+        }
+
+    /**
+     * returns map(service name -> service reference)
+     */
+    def allServices:Map[String,ServiceReference] = 
     {
         val allrefs = bc.getAllServiceReferences(null, null)
         if (allrefs == null) return Map()
 
-        // inner method used to return all the interface names a ServiceReference was registered under
-        def names(ref:ServiceReference):Array[String] = 
-        {
-            val n = ref.getProperty(OBJECTCLASS)
-            if (n != null) n.asInstanceOf[Array[String]] else Array()
-        }
-
-        // inner method used to return all the bundles using a given ServiceReference
-        def using(ref:ServiceReference):List[String] = 
-        {
-            val u = ref.getUsingBundles
-            if (u != null) u.toList.map(_ toString) else List()
-        }
-
-        //scan all service references to build a map of service name to list of using bundles
-        (for(ref <- bc.getAllServiceReferences(null, null);
-            name <- names(ref);
-            u = using(ref);
-            if (u.nonEmpty))
-            // yield (key,value) accumulates a list of (key,value) pairs
-            // the resulting list is transformed to a map and returned
-            yield (name, u)) toMap
+        // scan all service references to build a map of service name to list of using bundles
+        // yield (key,value) accumulates a list of (key,value) pairs
+        // the resulting list is transformed to a map and returned
+        (for {
+            ref <- bc.getAllServiceReferences(null, null)
+            name <- Option(ref.getProperty(OBJECTCLASS)).map(_.asInstanceOf[Array[String]]).getOrElse(Array())
+        } yield (name, ref)) toMap
     }
 }
diff --git a/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/shell/CLI.scala b/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/shell/CLI.scala
new file mode 100644
index 0000000..c3189d6
--- /dev/null
+++ b/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/shell/CLI.scala
@@ -0,0 +1,71 @@
+/*
+ * 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.servicediagnostics.shell
+
+import org.apache.felix.servicediagnostics.ServiceDiagnostics
+import org.apache.felix.servicediagnostics.Util._
+
+// old shell
+import org.apache.felix.shell.Command
+import java.io.PrintStream
+// gogo shell
+import org.apache.felix.service.command.Descriptor
+
+class CLI extends Command
+{
+    var engine:ServiceDiagnostics = _ //dependency injection. see Activator.
+
+    override def getName = "sd"
+    override def getShortDescription = "Service Diagnostics"
+    override def getUsage = "notavail|loops|using|providing"
+
+    // for gogo
+    def using = execute("sd using", System.out, System.err)
+    def providing = execute("sd providing", System.out, System.err)
+    def notavail = execute("sd notavail", System.out, System.err)
+    def loops = execute("sd loops", System.out, System.err)
+
+    // for old shell
+    override def execute(commandLine:String, out:PrintStream, err:PrintStream) = commandLine.split(" ").toList.tail match {
+        case "using"::Nil => 
+            out.println(json(engine.usingBundles).toString(2))
+        case "providing"::Nil => 
+            out.println(json(engine.serviceProviders).toString(2))
+        case "notavail"::Nil => 
+            out.println(json(engine.notavail).toString(2))
+        case "loops"::Nil => showloops(out)
+      case _ => err.println(getUsage)
+    }
+
+    def showloops(out:PrintStream) = {
+        val unresolved = engine.unresolved(false) // map(comp -> list(comp))
+        out.println(json(unresolved).toString(2))
+        def follow(n:String, stack:Set[String] = Set()) :Set[String] = 
+            if (stack contains n) stack 
+            else unresolved.get(n) match {
+                case None => stack
+                case Some(list) => list.toSet.flatMap { (d:String) => follow(d, stack+n) }
+            }
+        unresolved.keySet.map(follow(_)).foreach { loop => 
+            if (loop.size > 1 && unresolved(loop.last) == loop.head) 
+                out.println(loop.mkString("", " -> ", " -> "+loop.head)) 
+        }
+    }
+
+}
diff --git a/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/webconsole/WebConsolePlugin.scala b/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/webconsole/WebConsolePlugin.scala
index 2aaa6f4..34d181f 100644
--- a/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/webconsole/WebConsolePlugin.scala
+++ b/webconsole-plugins/servicediagnostics/core/src/main/scala/servicediagnostics/webconsole/WebConsolePlugin.scala
@@ -18,9 +18,6 @@
  */
 package org.apache.felix.servicediagnostics.webconsole
 
-import scala.collection.JavaConversions._
-import scala.collection.mutable.{Map => mMap}
-
 import java.io.PrintStream
 
 import javax.servlet.http._
@@ -31,6 +28,7 @@
 import org.apache.felix.webconsole.SimpleWebConsolePlugin
 
 import org.apache.felix.servicediagnostics.ServiceDiagnostics
+import org.apache.felix.servicediagnostics.Util._
 
 /**
  * This is the Apache Felix WebConsolePlugin implementation.
@@ -38,8 +36,7 @@
  *
  * @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
  */
-//class WebConsolePlugin extends SimpleWebConsolePlugin("servicegraph", "Service Graph", "OSGi", Array[String]()) 
-class WebConsolePlugin extends SimpleWebConsolePlugin("servicegraph", "Service Graph", Array[String]()) 
+class WebConsolePlugin extends SimpleWebConsolePlugin("servicegraph", "Service Graph", "OSGi", Array[String]()) 
 {
     var engine:ServiceDiagnostics = _ //dependency injection. see Activator.
 
@@ -57,16 +54,13 @@
      */
     override def doGet(req:HttpServletRequest, resp:HttpServletResponse) = 
         req.getPathInfo match {
-            case "/servicegraph/all" => resp.getWriter.println(json(engine.allServices))
+            case "/servicegraph/using" => resp.getWriter.println(json(engine.usingBundles))
+            case "/servicegraph/providing" => resp.getWriter.println(json(engine.serviceProviders))
             case "/servicegraph/notavail" => resp.getWriter.println(new JSONObject()
                                   .put("notavail", json(engine.notavail))
-                                  .put("unresolved", json(engine.unresolved)))
+                                  .put("unresolved", 
+                                      json(engine.unresolved(
+                                          Option(req.getParameter("optionals")).isDefined))))
             case x => super.doGet(req, resp)
           }
-
-    /** 
-     * turn the ServiceDiagnostics output into a JSON representation.
-     */
-    private def json(map:Map[String,List[AnyRef]]) = 
-      new JSONObject(asJavaMap(mMap() ++ map.map(kv => (kv._1, asJavaList(kv._2)))))
 }
diff --git a/webconsole-plugins/servicediagnostics/run.sh b/webconsole-plugins/servicediagnostics/run.sh
index 7c46988..fbb03d0 100755
--- a/webconsole-plugins/servicediagnostics/run.sh
+++ b/webconsole-plugins/servicediagnostics/run.sh
@@ -1,5 +1,5 @@
 REPO=$HOME/.m2/repository
-SCALA=$REPO/org/apache/servicemix/bundles/org.apache.servicemix.bundles.scala-library/2.9.1_3/org.apache.servicemix.bundles.scala-library-2.9.1_3.jar
+SCALA=$REPO/org/apache/servicemix/bundles/org.apache.servicemix.bundles.scala-library/2.10.0/org.apache.servicemix.bundles.scala-library-2.10.0.jar
 CLASSPATH=$SCALA:$REPO/org/apache/felix/org.apache.felix.main/4.0.3/org.apache.felix.main-4.0.3.jar:sample/target/servicediagnostics.sample-0.1.1-SNAPSHOT.jar
 #scala 
 java -classpath $CLASSPATH org.apache.felix.servicediagnostics.sample.FelixLauncher \
@@ -12,7 +12,7 @@
   $REPO/org/apache/felix/org.apache.felix.scr/1.6.0/org.apache.felix.scr-1.6.0.jar\
   $REPO/org/osgi/org.osgi.compendium/4.2.0/org.osgi.compendium-4.2.0.jar\
   $REPO/org/apache/felix/org.apache.felix.http.jetty/2.2.0/org.apache.felix.http.jetty-2.2.0.jar\
-  $REPO/org/apache/felix/org.apache.felix.webconsole/4.0.0/org.apache.felix.webconsole-4.0.0.jar\
+  $REPO/org/apache/felix/org.apache.felix.webconsole/4.2.0/org.apache.felix.webconsole-4.2.0.jar\
   $REPO/org/apache/felix/org.apache.felix.shell/1.4.3/org.apache.felix.shell-1.4.3.jar\
   $REPO/org/apache/commons/com.springsource.org.apache.commons.fileupload/1.2.1/com.springsource.org.apache.commons.fileupload-1.2.1.jar\
   $REPO/org/apache/commons/com.springsource.org.apache.commons.io/1.4.0/com.springsource.org.apache.commons.io-1.4.0.jar\
diff --git a/webconsole-plugins/servicediagnostics/sample/pom.xml b/webconsole-plugins/servicediagnostics/sample/pom.xml
index 64e638a..aed554b 100644
--- a/webconsole-plugins/servicediagnostics/sample/pom.xml
+++ b/webconsole-plugins/servicediagnostics/sample/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>org.apache.felix</groupId>
     <artifactId>servicediagnostics.parent</artifactId>
-    <version>0.1.1</version>
+    <version>0.1.3-SNAPSHOT</version>
     <relativePath>../parent/pom.xml</relativePath>
   </parent>
 
diff --git a/webconsole-plugins/servicediagnostics/sample/src/main/scala/servicediagnostics/sample/TestDM.scala b/webconsole-plugins/servicediagnostics/sample/src/main/scala/servicediagnostics/sample/TestDM.scala
index d0c3a18..c891927 100644
--- a/webconsole-plugins/servicediagnostics/sample/src/main/scala/servicediagnostics/sample/TestDM.scala
+++ b/webconsole-plugins/servicediagnostics/sample/src/main/scala/servicediagnostics/sample/TestDM.scala
@@ -154,7 +154,6 @@
     def start = try 
     {
         println("unavail="+diagnostics.notavail)
-        println("all="+diagnostics.allServices)
     }
     catch 
     {