Add support for OBR command. (FELIX-2042)


git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@942225 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/gogo/felixcommands/pom.xml b/gogo/felixcommands/pom.xml
index e026e84..b617519 100644
--- a/gogo/felixcommands/pom.xml
+++ b/gogo/felixcommands/pom.xml
@@ -50,6 +50,11 @@
       <artifactId>org.apache.felix.gogo.runtime</artifactId>
       <version>0.5.0-SNAPSHOT</version>
     </dependency>
+    <dependency>
+      <groupId>org.apache.felix</groupId>
+      <artifactId>org.apache.felix.bundlerepository</artifactId>
+      <version>1.6.0</version>
+    </dependency>
   </dependencies>
 
   <build>
@@ -74,6 +79,8 @@
             <Bundle-SymbolicName>${artifactId}</Bundle-SymbolicName>
             <Private-Package>${pom.artifactId}</Private-Package>
             <Bundle-Activator>${artifactId}.Activator</Bundle-Activator>
+            <Import-Package>!org.apache.felix.bundlerepository.*, *</Import-Package>
+            <DynamicImport-Package>org.apache.felix.bundlerepository, org.apache.felix.bundlerepository.*</DynamicImport-Package>
           </instructions>
         </configuration>
       </plugin>
diff --git a/gogo/felixcommands/src/main/java/org/apache/felix/gogo/felixcommands/Activator.java b/gogo/felixcommands/src/main/java/org/apache/felix/gogo/felixcommands/Activator.java
index 9ecd1e6..626a71f 100644
--- a/gogo/felixcommands/src/main/java/org/apache/felix/gogo/felixcommands/Activator.java
+++ b/gogo/felixcommands/src/main/java/org/apache/felix/gogo/felixcommands/Activator.java
@@ -21,9 +21,12 @@
 import java.util.Hashtable;
 import org.osgi.framework.BundleActivator;
 import org.osgi.framework.BundleContext;
+import org.osgi.util.tracker.ServiceTracker;
 
 public class Activator implements BundleActivator
 {
+    private volatile ServiceTracker m_tracker = null;
+
     public void start(BundleContext bc) throws Exception
     {
         Hashtable props = new Hashtable();
@@ -40,9 +43,19 @@
         props.put("osgi.command.function", new String[] { "ls" });
         bc.registerService(
             Files.class.getName(), new Files(bc), props);
+
+        m_tracker = new ServiceTracker(
+            bc, "org.apache.felix.bundlerepository.RepositoryAdmin", null);
+        m_tracker.open();
+        props.put("osgi.command.scope", "obr");
+        props.put("osgi.command.function", new String[] {
+            "deploy", "info", "javadoc", "list", "repos", "source" });
+        bc.registerService(
+            OBR.class.getName(), new OBR(bc, m_tracker), props);
     }
 
     public void stop(BundleContext bc) throws Exception
     {
+        m_tracker.close();
     }
 }
\ No newline at end of file
diff --git a/gogo/felixcommands/src/main/java/org/apache/felix/gogo/felixcommands/Basic.java b/gogo/felixcommands/src/main/java/org/apache/felix/gogo/felixcommands/Basic.java
index b7d56e8..13d70ad 100644
--- a/gogo/felixcommands/src/main/java/org/apache/felix/gogo/felixcommands/Basic.java
+++ b/gogo/felixcommands/src/main/java/org/apache/felix/gogo/felixcommands/Basic.java
@@ -30,6 +30,7 @@
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Comparator;
 import java.util.Date;
 import java.util.Dictionary;
 import java.util.Enumeration;
@@ -191,7 +192,7 @@
         {
             String title = Util.getBundleName(bundle);
             System.out.println("\n" + title);
-            System.out.println(Util.getUnderlineString(title));
+            System.out.println(Util.getUnderlineString(title.length()));
             Dictionary dict = bundle.getHeaders();
             Enumeration keys = dict.keys();
             while (keys.hasMoreElements())
@@ -218,7 +219,30 @@
         @Descriptor(description="target command") String name)
     {
         Map<String, List<Method>> commands = getCommands();
-        List<Method> methods = commands.get(name);
+
+        List<Method> methods = null;
+
+        // If the specified command doesn't have a scope, then
+        // search for matching methods by ignoring the scope.
+        int scopeIdx = name.indexOf(':');
+        if (scopeIdx < 0)
+        {
+            for (Entry<String, List<Method>> entry : commands.entrySet())
+            {
+                String k = entry.getKey().substring(entry.getKey().indexOf(':') + 1);
+                if (name.equals(k))
+                {
+                    methods = entry.getValue();
+                    break;
+                }
+            }
+        }
+        // Otherwise directly look up matching methods.
+        else
+        {
+            methods = commands.get(name);
+        }
+
         if ((methods != null) && (methods.size() > 0))
         {
             for (Method m : methods)
@@ -324,11 +348,12 @@
             Object svc = m_bc.getService(ref);
             if (svc != null)
             {
+                String scope = (String) ref.getProperty("osgi.command.scope");
                 String[] funcs = (String[]) ref.getProperty("osgi.command.function");
 
                 for (String func : funcs)
                 {
-                    commands.put(func, new ArrayList());
+                    commands.put(scope + ":" + func, new ArrayList());
                 }
 
                 if (!commands.isEmpty())
@@ -336,7 +361,7 @@
                     Method[] methods = svc.getClass().getMethods();
                     for (Method method : methods)
                     {
-                        List<Method> commandMethods = commands.get(method.getName());
+                        List<Method> commandMethods = commands.get(scope + ":" + method.getName());
                         if (commandMethods != null)
                         {
                             commandMethods.add(method);
diff --git a/gogo/felixcommands/src/main/java/org/apache/felix/gogo/felixcommands/Inspect.java b/gogo/felixcommands/src/main/java/org/apache/felix/gogo/felixcommands/Inspect.java
index d17c344..401ac1a 100644
--- a/gogo/felixcommands/src/main/java/org/apache/felix/gogo/felixcommands/Inspect.java
+++ b/gogo/felixcommands/src/main/java/org/apache/felix/gogo/felixcommands/Inspect.java
@@ -137,7 +137,7 @@
                         }
                         String title = bundles[bundleIdx] + " exports packages:";
                         System.out.println(title);
-                        System.out.println(Util.getUnderlineString(title));
+                        System.out.println(Util.getUnderlineString(title.length()));
                         if ((exports != null) && (exports.length > 0))
                         {
                             for (int expIdx = 0; expIdx < exports.length; expIdx++)
@@ -220,7 +220,7 @@
             ExportedPackage[] exports = pa.getExportedPackages((Bundle) null);
             String title = bundle + " imports packages:";
             System.out.println(title);
-            System.out.println(Util.getUnderlineString(title));
+            System.out.println(Util.getUnderlineString(title.length()));
             boolean found = false;
             for (int expIdx = 0; expIdx < exports.length; expIdx++)
             {
@@ -281,7 +281,7 @@
                                 }
                                 String title = bundles[bundleIdx] + " is required by:";
                                 System.out.println(title);
-                                System.out.println(Util.getUnderlineString(title));
+                                System.out.println(Util.getUnderlineString(title.length()));
                                 if ((rbs[rbIdx].getRequiringBundles() != null)
                                     && (rbs[rbIdx].getRequiringBundles().length > 0))
                                 {
@@ -356,7 +356,7 @@
             RequiredBundle[] rbs = pa.getRequiredBundles(null);
             String title = bundle + " requires bundles:";
             System.out.println(title);
-            System.out.println(Util.getUnderlineString(title));
+            System.out.println(Util.getUnderlineString(title.length()));
             boolean found = false;
             for (int rbIdx = 0; rbIdx < rbs.length; rbIdx++)
             {
@@ -410,7 +410,7 @@
                     {
                         String title = bundles[bundleIdx] + " is attached to:";
                         System.out.println(title);
-                        System.out.println(Util.getUnderlineString(title));
+                        System.out.println(Util.getUnderlineString(title.length()));
                         Bundle[] hosts = pa.getHosts(bundles[bundleIdx]);
                         for (int hostIdx = 0;
                             (hosts != null) && (hostIdx < hosts.length);
@@ -469,7 +469,7 @@
                     {
                         String title = bundles[bundleIdx] + " hosts:";
                         System.out.println(title);
-                        System.out.println(Util.getUnderlineString(title));
+                        System.out.println(Util.getUnderlineString(title.length()));
                         Bundle[] fragments = pa.getFragments(bundles[bundleIdx]);
                         for (int fragIdx = 0;
                             (fragments != null) && (fragIdx < fragments.length);
@@ -520,7 +520,7 @@
                     // Print header if we have not already done so.
                     String title = Util.getBundleName(bundles[bundleIdx]) + " provides services:";
                     System.out.println(title);
-                    System.out.println(Util.getUnderlineString(title));
+                    System.out.println(Util.getUnderlineString(title.length()));
 
                     if ((refs == null) || (refs.length == 0))
                     {
@@ -582,7 +582,7 @@
                     // Print header if we have not already done so.
                     String title = Util.getBundleName(bundles[bundleIdx]) + " requires services:";
                     System.out.println(title);
-                    System.out.println(Util.getUnderlineString(title));
+                    System.out.println(Util.getUnderlineString(title.length()));
 
                     if ((refs == null) || (refs.length == 0))
                     {
diff --git a/gogo/felixcommands/src/main/java/org/apache/felix/gogo/felixcommands/OBR.java b/gogo/felixcommands/src/main/java/org/apache/felix/gogo/felixcommands/OBR.java
new file mode 100644
index 0000000..e2205ab
--- /dev/null
+++ b/gogo/felixcommands/src/main/java/org/apache/felix/gogo/felixcommands/OBR.java
@@ -0,0 +1,637 @@
+/*
+ * 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.gogo.felixcommands;
+
+import java.io.*;
+import java.lang.reflect.Array;
+import java.net.URL;
+import java.util.*;
+import org.apache.felix.bundlerepository.Capability;
+import org.apache.felix.bundlerepository.Reason;
+
+import org.apache.felix.bundlerepository.RepositoryAdmin;
+import org.apache.felix.bundlerepository.Requirement;
+import org.apache.felix.bundlerepository.Resolver;
+import org.apache.felix.bundlerepository.Resource;
+import org.osgi.framework.*;
+import org.osgi.service.command.Descriptor;
+import org.osgi.service.command.Flag;
+import org.osgi.util.tracker.ServiceTracker;
+
+public class OBR
+{
+    private static final String REPO_ADD = "add";
+    private static final String REPO_REMOVE = "remove";
+    private static final String REPO_LIST = "list";
+    private static final String REPO_REFRESH = "refresh";
+
+    private static final char VERSION_SEPARATOR = '@';
+
+    private final BundleContext m_bc;
+    private final ServiceTracker m_tracker;
+
+    public OBR(BundleContext bc, ServiceTracker tracker)
+    {
+        m_bc = bc;
+        m_tracker = tracker;
+    }
+
+    private RepositoryAdmin getRepositoryAdmin()
+    {
+        Object svcObj;
+        try
+        {
+            svcObj = m_tracker.getService();
+        }
+        catch (Exception ex)
+        {
+            svcObj = null;
+        }
+        if (svcObj == null)
+        {
+            System.out.println("No repository admin service available");
+        }
+        return (RepositoryAdmin) svcObj;
+    }
+
+    @Descriptor(description="manage repositories")
+    public void repos(
+        @Descriptor(description="( add | list | refresh | remove )") String action,
+        @Descriptor(description="space-delimited list of repository URLs") String[] args)
+        throws IOException
+    {
+        Object svcObj = getRepositoryAdmin();
+        if (svcObj == null)
+        {
+            return;
+        }
+        RepositoryAdmin ra = (RepositoryAdmin) svcObj;
+
+        if (args.length > 0)
+        {
+            for (int i = 0; i < args.length; i++)
+            {
+                try
+                {
+                    if (action.equals(REPO_ADD))
+                    {
+                        ra.addRepository(args[i]);
+                    }
+                    else if (action.equals(REPO_REFRESH))
+                    {
+                        ra.removeRepository(args[i]);
+                        ra.addRepository(args[i]);
+                    }
+                    else if (action.equals(REPO_REMOVE))
+                    {
+                        ra.removeRepository(args[i]);
+                    }
+                    else
+                    {
+                        System.out.println("Unknown repository operation: " + action);
+                    }
+                }
+                catch (Exception ex)
+                {
+                    ex.printStackTrace(System.err);
+                }
+            }
+        }
+        else
+        {
+            org.apache.felix.bundlerepository.Repository[] repos =
+                ra.listRepositories();
+            if ((repos != null) && (repos.length > 0))
+            {
+                for (int i = 0; i < repos.length; i++)
+                {
+                    System.out.println(repos[i].getURI());
+                }
+            }
+            else
+            {
+                System.out.println("No repository URLs are set.");
+            }
+        }
+    }
+
+    @Descriptor(description="list repository resources")
+    public void list(
+        @Flag(name="-v", description="verbose") boolean verbose,
+        @Descriptor(description="optional strings used for name matching") String[] args)
+        throws IOException, InvalidSyntaxException
+    {
+        Object svcObj = getRepositoryAdmin();
+        if (svcObj == null)
+        {
+            return;
+        }
+        RepositoryAdmin ra = (RepositoryAdmin) svcObj;
+
+        // Create a filter that will match presentation name or symbolic name.
+        StringBuffer sb = new StringBuffer();
+        if ((args == null) || (args.length == 0))
+        {
+            sb.append("(|(presentationname=*)(symbolicname=*))");
+        }
+        else
+        {
+            StringBuffer value = new StringBuffer();
+            for (int i = 0; i < args.length; i++)
+            {
+                if (i > 0)
+                {
+                    value.append(" ");
+                }
+                value.append(args[i]);
+            }
+            sb.append("(|(presentationname=*");
+            sb.append(value);
+            sb.append("*)(symbolicname=*");
+            sb.append(value);
+            sb.append("*))");
+        }
+        // Use filter to get matching resources.
+        Resource[] resources = ra.discoverResources(sb.toString());
+
+        // Group the resources by symbolic name in descending version order,
+        // but keep them in overall sorted order by presentation name.
+        Map revisionMap = new TreeMap(new Comparator() {
+            public int compare(Object o1, Object o2)
+            {
+                Resource r1 = (Resource) o1;
+                Resource r2 = (Resource) o2;
+                // Assume if the symbolic name is equal, then the two are equal,
+                // since we are trying to aggregate by symbolic name.
+                int symCompare = r1.getSymbolicName().compareTo(r2.getSymbolicName());
+                if (symCompare == 0)
+                {
+                    return 0;
+                }
+                // Otherwise, compare the presentation name to keep them sorted
+                // by presentation name. If the presentation names are equal, then
+                // use the symbolic name to differentiate.
+                int compare = (r1.getPresentationName() == null)
+                    ? -1
+                    : (r2.getPresentationName() == null)
+                        ? 1
+                        : r1.getPresentationName().compareToIgnoreCase(
+                            r2.getPresentationName());
+                if (compare == 0)
+                {
+                    return symCompare;
+                }
+                return compare;
+            }
+        });
+        for (int resIdx = 0; (resources != null) && (resIdx < resources.length); resIdx++)
+        {
+            Resource[] revisions = (Resource[]) revisionMap.get(resources[resIdx]);
+            revisionMap.put(resources[resIdx], addResourceByVersion(revisions, resources[resIdx]));
+        }
+
+        // Print any matching resources.
+        for (Iterator i = revisionMap.entrySet().iterator(); i.hasNext(); )
+        {
+            Map.Entry entry = (Map.Entry) i.next();
+            Resource[] revisions = (Resource[]) entry.getValue();
+            String name = revisions[0].getPresentationName();
+            name = (name == null) ? revisions[0].getSymbolicName() : name;
+            System.out.print(name);
+
+            if (verbose && revisions[0].getPresentationName() != null)
+            {
+                System.out.print(" [" + revisions[0].getSymbolicName() + "]");
+            }
+
+            System.out.print(" (");
+            int revIdx = 0;
+            do
+            {
+                if (revIdx > 0)
+                {
+                    System.out.print(", ");
+                }
+                System.out.print(revisions[revIdx].getVersion());
+                revIdx++;
+            }
+            while (verbose && (revIdx < revisions.length));
+            if (!verbose && (revisions.length > 1))
+            {
+                System.out.print(", ...");
+            }
+            System.out.println(")");
+        }
+
+        if ((resources == null) || (resources.length == 0))
+        {
+            System.out.println("No matching bundles.");
+        }
+    }
+
+    @Descriptor(description="retrieve resource description from repository")
+    public void info(
+        @Descriptor(description="( <bundle-name> | <symbolic-name> | <bundle-id> )[@<version>] ...")
+            String[] args)
+        throws IOException, InvalidSyntaxException
+    {
+        Object svcObj = getRepositoryAdmin();
+        if (svcObj == null)
+        {
+            return;
+        }
+        RepositoryAdmin ra = (RepositoryAdmin) svcObj;
+
+        for (int argIdx = 0; (args != null) && (argIdx < args.length); argIdx++)
+        {
+            // Find the target's bundle resource.
+            String targetName = args[argIdx];
+            String targetVersion = null;
+            int idx = args[argIdx].indexOf(VERSION_SEPARATOR);
+            if (idx > 0)
+            {
+                targetName = args[argIdx].substring(0, idx);
+                targetVersion = args[argIdx].substring(idx + 1);
+            }
+            Resource[] resources = searchRepository(ra, targetName, targetVersion);
+            if (resources == null)
+            {
+                System.err.println("Unknown bundle and/or version: " + args[argIdx]);
+            }
+            else
+            {
+                for (int resIdx = 0; resIdx < resources.length; resIdx++)
+                {
+                    if (resIdx > 0)
+                    {
+                        System.out.println("");
+                    }
+                    printResource(System.out, resources[resIdx]);
+                }
+            }
+        }
+    }
+
+    @Descriptor(description="deploy resource from repository")
+    public void deploy(
+        @Flag(name="-s", description="start deployed bundles") boolean start,
+        @Descriptor(description="( <bundle-name> | <symbolic-name> | <bundle-id> )[@<version>] ...")
+            String[] args)
+        throws IOException, InvalidSyntaxException
+    {
+        Object svcObj = getRepositoryAdmin();
+        if (svcObj == null)
+        {
+            return;
+        }
+        RepositoryAdmin ra = (RepositoryAdmin) svcObj;
+
+        Resolver resolver = ra.resolver();
+        for (int argIdx = 0; (args != null) && (argIdx < args.length); argIdx++)
+        {
+            // Find the target's bundle resource.
+            String targetName = args[argIdx];
+            String targetVersion = null;
+            int idx = args[argIdx].indexOf(VERSION_SEPARATOR);
+            if (idx > 0)
+            {
+                targetName = args[argIdx].substring(0, idx);
+                targetVersion = args[argIdx].substring(idx + 1);
+            }
+            Resource resource = selectNewestVersion(
+                searchRepository(ra, targetName, targetVersion));
+            if (resource != null)
+            {
+                resolver.add(resource);
+            }
+            else
+            {
+                System.err.println("Unknown bundle - " + args[argIdx]);
+            }
+        }
+
+        if ((resolver.getAddedResources() != null) &&
+            (resolver.getAddedResources().length > 0))
+        {
+            if (resolver.resolve())
+            {
+                System.out.println("Target resource(s):");
+                System.out.println(Util.getUnderlineString(19));
+                Resource[] resources = resolver.getAddedResources();
+                for (int resIdx = 0; (resources != null) && (resIdx < resources.length); resIdx++)
+                {
+                    System.out.println("   " + resources[resIdx].getPresentationName()
+                        + " (" + resources[resIdx].getVersion() + ")");
+                }
+                resources = resolver.getRequiredResources();
+                if ((resources != null) && (resources.length > 0))
+                {
+                    System.out.println("\nRequired resource(s):");
+                    System.out.println(Util.getUnderlineString(21));
+                    for (int resIdx = 0; resIdx < resources.length; resIdx++)
+                    {
+                        System.out.println("   " + resources[resIdx].getPresentationName()
+                            + " (" + resources[resIdx].getVersion() + ")");
+                    }
+                }
+                resources = resolver.getOptionalResources();
+                if ((resources != null) && (resources.length > 0))
+                {
+                    System.out.println("\nOptional resource(s):");
+                    System.out.println(Util.getUnderlineString(21));
+                    for (int resIdx = 0; resIdx < resources.length; resIdx++)
+                    {
+                        System.out.println("   " + resources[resIdx].getPresentationName()
+                            + " (" + resources[resIdx].getVersion() + ")");
+                    }
+                }
+
+                try
+                {
+                    System.out.print("\nDeploying...");
+                    resolver.deploy(start ? Resolver.START : 0);
+                    System.out.println("done.");
+                }
+                catch (IllegalStateException ex)
+                {
+                    System.err.println(ex);
+                }
+            }
+            else
+            {
+                Reason[] reqs = resolver.getUnsatisfiedRequirements();
+                if ((reqs != null) && (reqs.length > 0))
+                {
+                    System.out.println("Unsatisfied requirement(s):");
+                    System.out.println(Util.getUnderlineString(27));
+                    for (int reqIdx = 0; reqIdx < reqs.length; reqIdx++)
+                    {
+                        System.out.println("   " + reqs[reqIdx].getRequirement().getFilter());
+                        System.out.println("      " + reqs[reqIdx].getResource().getPresentationName());
+                    }
+                }
+                else
+                {
+                    System.out.println("Could not resolve targets.");
+                }
+            }
+        }
+    }
+
+    @Descriptor(description="retrieve resource source code from repository")
+    public void source(
+        @Flag(name="-x", description="extract") boolean extract,
+        @Descriptor(description="local target directory") File localDir,
+        @Descriptor(description="( <bundle-name> | <symbolic-name> | <bundle-id> )[@<version>] ...")
+            String[] args)
+        throws IOException, InvalidSyntaxException
+    {
+        Object svcObj = getRepositoryAdmin();
+        if (svcObj == null)
+        {
+            return;
+        }
+        RepositoryAdmin ra = (RepositoryAdmin) svcObj;
+
+        for (int argIdx = 0; argIdx < args.length; argIdx++)
+        {
+            // Find the target's bundle resource.
+            String targetName = args[argIdx];
+            String targetVersion = null;
+            int idx = args[argIdx].indexOf(VERSION_SEPARATOR);
+            if (idx > 0)
+            {
+                targetName = args[argIdx].substring(0, idx);
+                targetVersion = args[argIdx].substring(idx + 1);
+            }
+            Resource resource = selectNewestVersion(
+                searchRepository(ra, targetName, targetVersion));
+            if (resource == null)
+            {
+                System.err.println("Unknown bundle and/or version: " + args[argIdx]);
+            }
+            else
+            {
+                String srcURI = (String) resource.getProperties().get(Resource.SOURCE_URI);
+                if (srcURI != null)
+                {
+                    Util.downloadSource(
+                        System.out, System.err, new URL(srcURI),
+                        localDir, extract);
+                }
+                else
+                {
+                    System.err.println("Missing source URL: " + args[argIdx]);
+                }
+            }
+        }
+    }
+
+    @Descriptor(description="retrieve resource JavaDoc from repository")
+    public void javadoc(
+        @Flag(name="-x", description="extract") boolean extract,
+        @Descriptor(description="local target directory") File localDir,
+        @Descriptor(description="( <bundle-name> | <symbolic-name> | <bundle-id> )[@<version>] ...")
+            String[] args)
+        throws IOException, InvalidSyntaxException
+    {
+        Object svcObj = getRepositoryAdmin();
+        if (svcObj == null)
+        {
+            return;
+        }
+        RepositoryAdmin ra = (RepositoryAdmin) svcObj;
+
+        for (int argIdx = 0; argIdx < args.length; argIdx++)
+        {
+            // Find the target's bundle resource.
+            String targetName = args[argIdx];
+            String targetVersion = null;
+            int idx = args[argIdx].indexOf(VERSION_SEPARATOR);
+            if (idx > 0)
+            {
+                targetName = args[argIdx].substring(0, idx);
+                targetVersion = args[argIdx].substring(idx + 1);
+            }
+            Resource resource = selectNewestVersion(
+                searchRepository(ra, targetName, targetVersion));
+            if (resource == null)
+            {
+                System.err.println("Unknown bundle and/or version: " + args[argIdx]);
+            }
+            else
+            {
+                URL docURL = (URL) resource.getProperties().get("javadoc");
+                if (docURL != null)
+                {
+                    Util.downloadSource(
+                        System.out, System.err, docURL, localDir, extract);
+                }
+                else
+                {
+                    System.err.println("Missing javadoc URL: " + args[argIdx]);
+                }
+            }
+        }
+    }
+
+    private Resource[] searchRepository(
+        RepositoryAdmin ra, String targetId, String targetVersion)
+        throws InvalidSyntaxException
+    {
+        // Try to see if the targetId is a bundle ID.
+        try
+        {
+            Bundle bundle = m_bc.getBundle(Long.parseLong(targetId));
+            targetId = bundle.getSymbolicName();
+        }
+        catch (NumberFormatException ex)
+        {
+            // It was not a number, so ignore.
+        }
+
+        // The targetId may be a bundle name or a bundle symbolic name,
+        // so create the appropriate LDAP query.
+        StringBuffer sb = new StringBuffer("(|(presentationname=");
+        sb.append(targetId);
+        sb.append(")(symbolicname=");
+        sb.append(targetId);
+        sb.append("))");
+        if (targetVersion != null)
+        {
+            sb.insert(0, "(&");
+            sb.append("(version=");
+            sb.append(targetVersion);
+            sb.append("))");
+        }
+        return ra.discoverResources(sb.toString());
+    }
+
+    private Resource selectNewestVersion(Resource[] resources)
+    {
+        int idx = -1;
+        Version v = null;
+        for (int i = 0; (resources != null) && (i < resources.length); i++)
+        {
+            if (i == 0)
+            {
+                idx = 0;
+                v = resources[i].getVersion();
+            }
+            else
+            {
+                Version vtmp = resources[i].getVersion();
+                if (vtmp.compareTo(v) > 0)
+                {
+                    idx = i;
+                    v = vtmp;
+                }
+            }
+        }
+
+        return (idx < 0) ? null : resources[idx];
+    }
+
+    private void printResource(PrintStream out, Resource resource)
+    {
+        System.out.println(Util.getUnderlineString(resource.getPresentationName().length()));
+        out.println(resource.getPresentationName());
+        System.out.println(Util.getUnderlineString(resource.getPresentationName().length()));
+
+        Map map = resource.getProperties();
+        for (Iterator iter = map.entrySet().iterator(); iter.hasNext(); )
+        {
+            Map.Entry entry = (Map.Entry) iter.next();
+            if (entry.getValue().getClass().isArray())
+            {
+                out.println(entry.getKey() + ":");
+                for (int j = 0; j < Array.getLength(entry.getValue()); j++)
+                {
+                    out.println("   " + Array.get(entry.getValue(), j));
+                }
+            }
+            else
+            {
+                out.println(entry.getKey() + ": " + entry.getValue());
+            }
+        }
+
+        Requirement[] reqs = resource.getRequirements();
+        if ((reqs != null) && (reqs.length > 0))
+        {
+            out.println("Requires:");
+            for (int i = 0; i < reqs.length; i++)
+            {
+                out.println("   " + reqs[i].getFilter());
+            }
+        }
+
+        Capability[] caps = resource.getCapabilities();
+        if ((caps != null) && (caps.length > 0))
+        {
+            out.println("Capabilities:");
+            for (int i = 0; i < caps.length; i++)
+            {
+                out.println("   " + caps[i].getPropertiesAsMap());
+            }
+        }
+    }
+
+    private static Resource[] addResourceByVersion(Resource[] revisions, Resource resource)
+    {
+        // We want to add the resource into the array of revisions
+        // in descending version sorted order (i.e., newest first)
+        Resource[] sorted = null;
+        if (revisions == null)
+        {
+            sorted = new Resource[] { resource };
+        }
+        else
+        {
+            Version version = resource.getVersion();
+            Version middleVersion = null;
+            int top = 0, bottom = revisions.length - 1, middle = 0;
+            while (top <= bottom)
+            {
+                middle = (bottom - top) / 2 + top;
+                middleVersion = revisions[middle].getVersion();
+                // Sort in reverse version order.
+                int cmp = middleVersion.compareTo(version);
+                if (cmp < 0)
+                {
+                    bottom = middle - 1;
+                }
+                else
+                {
+                    top = middle + 1;
+                }
+            }
+
+            // Ignore duplicates.
+            if ((top >= revisions.length) || (revisions[top] != resource))
+            {
+                sorted = new Resource[revisions.length + 1];
+                System.arraycopy(revisions, 0, sorted, 0, top);
+                System.arraycopy(revisions, top, sorted, top + 1, revisions.length - top);
+                sorted[top] = resource;
+            }
+        }
+        return sorted;
+    }
+}
\ No newline at end of file
diff --git a/gogo/felixcommands/src/main/java/org/apache/felix/gogo/felixcommands/Util.java b/gogo/felixcommands/src/main/java/org/apache/felix/gogo/felixcommands/Util.java
index 03021dc..d3602bc 100644
--- a/gogo/felixcommands/src/main/java/org/apache/felix/gogo/felixcommands/Util.java
+++ b/gogo/felixcommands/src/main/java/org/apache/felix/gogo/felixcommands/Util.java
@@ -18,7 +18,21 @@
  */
 package org.apache.felix.gogo.felixcommands;
 
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.net.URL;
+import java.net.URLConnection;
 import java.util.List;
+import java.util.jar.JarEntry;
+import java.util.jar.JarInputStream;
+import org.apache.felix.bundlerepository.impl.Base64Encoder;
+import org.apache.felix.bundlerepository.impl.FileUtil;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.Constants;
@@ -39,12 +53,12 @@
     }
     private final static StringBuffer m_sb = new StringBuffer();
 
-    public static String getUnderlineString(String s)
+    public static String getUnderlineString(int len)
     {
         synchronized (m_sb)
         {
             m_sb.delete(0, m_sb.length());
-            for (int i = 0; i < s.length(); i++)
+            for (int i = 0; i < len; i++)
             {
                 m_sb.append('-');
             }
@@ -132,4 +146,176 @@
             bc.ungetService(refs.remove(0));
         }
     }
+
+    public static void downloadSource(
+        PrintStream out, PrintStream err,
+        URL srcURL, File localDir, boolean extract)
+    {
+        // Get the file name from the URL.
+        String fileName = (srcURL.getFile().lastIndexOf('/') > 0)
+            ? srcURL.getFile().substring(srcURL.getFile().lastIndexOf('/') + 1)
+            : srcURL.getFile();
+
+        try
+        {
+            out.println("Connecting...");
+
+            if (!localDir.exists())
+            {
+                err.println("Destination directory does not exist.");
+            }
+            File file = new File(localDir, fileName);
+
+            OutputStream os = new FileOutputStream(file);
+            URLConnection conn = srcURL.openConnection();
+            Util.setProxyAuth(conn);
+            int total = conn.getContentLength();
+            InputStream is = conn.getInputStream();
+
+            if (total > 0)
+            {
+                out.println("Downloading " + fileName
+                    + " ( " + total + " bytes ).");
+            }
+            else
+            {
+                out.println("Downloading " + fileName + ".");
+            }
+            byte[] buffer = new byte[4096];
+            int count = 0;
+            for (int len = is.read(buffer); len > 0; len = is.read(buffer))
+            {
+                count += len;
+                os.write(buffer, 0, len);
+            }
+
+            os.close();
+            is.close();
+
+            if (extract)
+            {
+                is = new FileInputStream(file);
+                JarInputStream jis = new JarInputStream(is);
+                out.println("Extracting...");
+                unjar(jis, localDir);
+                jis.close();
+                file.delete();
+            }
+        }
+        catch (Exception ex)
+        {
+            err.println(ex);
+        }
+    }
+
+    public static void unjar(JarInputStream jis, File dir)
+        throws IOException
+    {
+        // Reusable buffer.
+        byte[] buffer = new byte[4096];
+
+        // Loop through JAR entries.
+        for (JarEntry je = jis.getNextJarEntry();
+             je != null;
+             je = jis.getNextJarEntry())
+        {
+            if (je.getName().startsWith("/"))
+            {
+                throw new IOException("JAR resource cannot contain absolute paths.");
+            }
+
+            File target = new File(dir, je.getName());
+
+            // Check to see if the JAR entry is a directory.
+            if (je.isDirectory())
+            {
+                if (!target.exists())
+                {
+                    if (!target.mkdirs())
+                    {
+                        throw new IOException("Unable to create target directory: "
+                            + target);
+                    }
+                }
+                // Just continue since directories do not have content to copy.
+                continue;
+            }
+
+            int lastIndex = je.getName().lastIndexOf('/');
+            String name = (lastIndex >= 0) ?
+                je.getName().substring(lastIndex + 1) : je.getName();
+            String destination = (lastIndex >= 0) ?
+                je.getName().substring(0, lastIndex) : "";
+
+            // JAR files use '/', so convert it to platform separator.
+            destination = destination.replace('/', File.separatorChar);
+            copy(jis, dir, name, destination, buffer);
+        }
+    }
+
+    public static void copy(
+        InputStream is, File dir, String destName, String destDir, byte[] buffer)
+        throws IOException
+    {
+        if (destDir == null)
+        {
+            destDir = "";
+        }
+
+        // Make sure the target directory exists and
+        // that is actually a directory.
+        File targetDir = new File(dir, destDir);
+        if (!targetDir.exists())
+        {
+            if (!targetDir.mkdirs())
+            {
+                throw new IOException("Unable to create target directory: "
+                    + targetDir);
+            }
+        }
+        else if (!targetDir.isDirectory())
+        {
+            throw new IOException("Target is not a directory: "
+                + targetDir);
+        }
+
+        BufferedOutputStream bos = new BufferedOutputStream(
+            new FileOutputStream(new File(targetDir, destName)));
+        int count = 0;
+        while ((count = is.read(buffer)) > 0)
+        {
+            bos.write(buffer, 0, count);
+        }
+        bos.close();
+    }
+
+    public static void setProxyAuth(URLConnection conn) throws IOException
+    {
+        // Support for http proxy authentication
+        String auth = System.getProperty("http.proxyAuth");
+        if ((auth != null) && (auth.length() > 0))
+        {
+            if ("http".equals(conn.getURL().getProtocol())
+                || "https".equals(conn.getURL().getProtocol()))
+            {
+                String base64 = Base64Encoder.base64Encode(auth);
+                conn.setRequestProperty("Proxy-Authorization", "Basic " + base64);
+            }
+        }
+    }
+
+    public static InputStream openURL(final URL url) throws IOException
+    {
+        // Do it the manual way to have a chance to
+        // set request properties as proxy auth (EW).
+        return openURL(url.openConnection());
+    }
+
+    public static InputStream openURL(final URLConnection conn) throws IOException
+    {
+        // Do it the manual way to have a chance to
+        // set request properties as proxy auth (EW).
+        setProxyAuth(conn);
+        return conn.getInputStream();
+    }
 }
\ No newline at end of file