Added subproject for the Shell GUI Plugin, which contains some plugins for
the Shell GUI bundle for a simple GUI shell for Felix.


git-svn-id: https://svn.apache.org/repos/asf/incubator/felix/trunk@391623 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/org.apache.felix.shell.gui.plugin/pom.xml b/org.apache.felix.shell.gui.plugin/pom.xml
new file mode 100644
index 0000000..121e833
--- /dev/null
+++ b/org.apache.felix.shell.gui.plugin/pom.xml
@@ -0,0 +1,61 @@
+<project>
+  <parent>
+    <groupId>org.apache.felix</groupId>
+    <artifactId>felix</artifactId>
+    <version>0.8.0-SNAPSHOT</version>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+  <packaging>osgi-bundle</packaging>
+  <name>Apache Felix Shell GUI Plugin</name>
+  <artifactId>org.apache.felix.shell.gui.plugin</artifactId>
+  <dependencies>
+    <dependency>
+      <groupId>${pom.groupId}</groupId>
+      <artifactId>org.osgi.core</artifactId>
+      <version>${pom.version}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>${pom.groupId}</groupId>
+      <artifactId>org.apache.felix.shell</artifactId>
+      <version>${pom.version}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>${pom.groupId}</groupId>
+      <artifactId>org.apache.felix.bundlerepository</artifactId>
+      <version>${pom.version}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>${pom.groupId}</groupId>
+      <artifactId>org.apache.felix.shell.gui</artifactId>
+      <version>${pom.version}</version>
+      <scope>provided</scope>
+    </dependency>
+  </dependencies>
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.felix.plugins</groupId>
+        <artifactId>maven-osgi-plugin</artifactId>
+        <version>${pom.version}</version>
+        <extensions>true</extensions>
+        <configuration>
+          <osgiManifest>
+            <bundleName>ShellGUIPlugin</bundleName>
+            <bundleDescription>A simple set of plugins for the ShellGUI bundle.</bundleDescription>
+            <bundleActivator>org.apache.felix.shell.gui.plugin.Activator</bundleActivator>
+            <bundleDocUrl>http://oscar-osgi.sf.net/obr2/shellguiplugin/</bundleDocUrl>
+            <bundleUrl>http://oscar-osgi.sf.net/obr2/shellguiplugin/org.apache.felix.shell.gui.plugin.jar</bundleUrl>
+            <bundleSource>http://oscar-osgi.sf.net/obr2/shellguiplugin/org.apache.felix.shell.gui.plugin-src.jar</bundleSource>
+            <bundleSymbolicName>org.apache.felix.shell.gui.plugin</bundleSymbolicName>
+            <importPackage>org.osgi.framework,javax.swing,javax.swing.event,javax.swing.table,javax.swing.tree,javax.swing.border,javax.swing.text,org.apache.felix.shell,org.apache.felix.shell.gui,org.osgi.service.obr</importPackage>
+            <exportService>org.apache.felix.shell.gui.Plugin</exportService>
+            <importService>org.apache.felix.shell.ShellService,org.apache.felix.bundlerepository.RepostioryAdmin</importService>
+          </osgiManifest>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/org.apache.felix.shell.gui.plugin/src/main/java/org/apache/felix/shell/gui/plugin/Activator.java b/org.apache.felix.shell.gui.plugin/src/main/java/org/apache/felix/shell/gui/plugin/Activator.java
new file mode 100644
index 0000000..b1fdeb6
--- /dev/null
+++ b/org.apache.felix.shell.gui.plugin/src/main/java/org/apache/felix/shell/gui/plugin/Activator.java
@@ -0,0 +1,49 @@
+/*
+ *   Copyright 2006 The Apache Software Foundation
+ *
+ *   Licensed 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.shell.gui.plugin;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+public class Activator implements BundleActivator
+{
+    private BundleContext m_context = null;
+
+    public void start(BundleContext context)
+    {
+        m_context = context;
+
+        // Register the bundle list plugin.
+        m_context.registerService(
+            org.apache.felix.shell.gui.Plugin.class.getName(),
+            new BundleListPlugin(m_context), null);
+
+        // Register the shell plugin.
+        m_context.registerService(
+            org.apache.felix.shell.gui.Plugin.class.getName(),
+            new ShellPlugin(m_context), null);
+
+        // Register the OBR plugin.
+        m_context.registerService(
+            org.apache.felix.shell.gui.Plugin.class.getName(),
+            new OBRPlugin(m_context), null);
+    }
+
+    public void stop(BundleContext context)
+    {
+    }
+}
\ No newline at end of file
diff --git a/org.apache.felix.shell.gui.plugin/src/main/java/org/apache/felix/shell/gui/plugin/BundleListPlugin.java b/org.apache.felix.shell.gui.plugin/src/main/java/org/apache/felix/shell/gui/plugin/BundleListPlugin.java
new file mode 100644
index 0000000..2933861
--- /dev/null
+++ b/org.apache.felix.shell.gui.plugin/src/main/java/org/apache/felix/shell/gui/plugin/BundleListPlugin.java
@@ -0,0 +1,342 @@
+/*
+ *   Copyright 2006 The Apache Software Foundation
+ *
+ *   Licensed 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.shell.gui.plugin;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.*;
+import javax.swing.table.AbstractTableModel;
+
+import org.apache.felix.shell.gui.Plugin;
+import org.osgi.framework.*;
+import org.osgi.service.packageadmin.PackageAdmin;
+
+public class BundleListPlugin extends JPanel implements Plugin
+{
+    private BundleContext m_context = null;
+    private JTextField m_urlField = null;
+    private JButton m_installButton = null;
+    private JTable m_bundleTable = null;
+    private JButton m_startButton = null;
+    private JButton m_stopButton = null;
+    private JButton m_updateButton = null;
+    private JButton m_refreshButton = null;
+    private JButton m_uninstallButton = null;
+
+    // Plugin interface methods.
+
+    public String getName()
+    {
+        return "Bundle List";    
+    }
+    
+    public Component getGUI()
+    {
+        return this;
+    }
+    
+    // Implementation.
+    
+    public BundleListPlugin(BundleContext context)
+    {
+        m_context = context;
+
+        // Create user interface components.
+        setLayout(new BorderLayout());
+        JScrollPane scroll = null;
+        add(createURLPanel(), BorderLayout.NORTH);
+        add(scroll = new JScrollPane(m_bundleTable = new JTable()), BorderLayout.CENTER);
+        add(createButtonPanel(), BorderLayout.SOUTH);
+
+        // Set table model to display bundles.
+        m_bundleTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
+        m_bundleTable.setModel(new SimpleTableModel());
+        m_bundleTable.getColumnModel().getColumn(0).setPreferredWidth(75);
+        m_bundleTable.getColumnModel().getColumn(1).setPreferredWidth(100);
+        m_bundleTable.getColumnModel().getColumn(2).setPreferredWidth(350);
+
+        createEventListeners();
+    }
+
+    private JPanel createURLPanel()
+    {
+        JPanel panel = new JPanel(new BorderLayout());
+        panel.add(new JLabel("URL"), BorderLayout.WEST);
+        panel.add(m_urlField = new JTextField(20), BorderLayout.CENTER);
+        panel.add(m_installButton = new JButton("Install"), BorderLayout.EAST);
+        m_installButton.setMnemonic('I');
+        return panel;
+    }
+
+    private JPanel createButtonPanel()
+    {
+        JPanel panel = new JPanel();
+        panel.add(m_startButton = new JButton("Start"));
+        panel.add(m_stopButton = new JButton("Stop"));
+        panel.add(m_updateButton = new JButton("Update"));
+        panel.add(m_refreshButton = new JButton("Refresh"));
+        panel.add(m_uninstallButton = new JButton("Uninstall"));
+        m_startButton.setMnemonic('S');
+        m_stopButton.setMnemonic('p');
+        m_updateButton.setMnemonic('a');
+        m_refreshButton.setMnemonic('R');
+        m_uninstallButton.setMnemonic('U');
+        return panel;
+    }
+
+    private void createEventListeners()
+    {
+        // Listen for bundle events in order to update
+        // the GUI bundle list.
+        BundleListener bl = new BundleListener() {
+            public void bundleChanged(BundleEvent event)
+            {
+                ((SimpleTableModel) m_bundleTable.getModel()).update();
+            }
+        };
+        m_context.addBundleListener(bl);
+
+        // Create action listeners.
+        m_installButton.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent event)
+            {
+                if (m_urlField.getText().length() > 0)
+                {
+                    try
+                    {
+                        m_context.installBundle(m_urlField.getText(), null);
+                    }
+                    catch (BundleException ex)
+                    {
+                        JOptionPane.showMessageDialog(
+                            JOptionPane.getFrameForComponent(BundleListPlugin.this),
+                            ex.getMessage(), "Error",
+                            JOptionPane.ERROR_MESSAGE);
+                    }
+                }
+            }
+        });
+
+        m_startButton.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent event)
+            {
+                int[] rows = m_bundleTable.getSelectedRows();
+                for (int i = 0; i < rows.length; i++)
+                {
+                    try
+                    {
+                        m_context.getBundles()[rows[i]].start();
+                    }
+                    catch (BundleException ex)
+                    {
+                        JOptionPane.showMessageDialog(
+                            JOptionPane.getFrameForComponent(BundleListPlugin.this),
+                            ex.getMessage(), "Error",
+                            JOptionPane.ERROR_MESSAGE);
+                    }
+                }
+            }
+        });
+
+        m_stopButton.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent event)
+            {
+                int[] rows = m_bundleTable.getSelectedRows();
+                for (int i = 0; i < rows.length; i++)
+                {
+                    try
+                    {
+                        m_context.getBundles()[rows[i]].stop();
+                    }
+                    catch (BundleException ex)
+                    {
+                        JOptionPane.showMessageDialog(
+                            JOptionPane.getFrameForComponent(BundleListPlugin.this),
+                            ex.getMessage(), "Error",
+                            JOptionPane.ERROR_MESSAGE);
+                    }
+                }
+            }
+        });
+
+        m_updateButton.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent event)
+            {
+                int[] rows = m_bundleTable.getSelectedRows();
+                for (int i = 0; i < rows.length; i++)
+                {
+                    try
+                    {
+                        m_context.getBundles()[rows[i]].update();
+                    }
+                    catch (BundleException ex)
+                    {
+                        JOptionPane.showMessageDialog(
+                            JOptionPane.getFrameForComponent(BundleListPlugin.this),
+                            ex.getMessage(), "Error",
+                            JOptionPane.ERROR_MESSAGE);
+                    }
+                }
+            }
+        });
+
+        m_refreshButton.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent event)
+            {
+                // Get package admin service.
+                ServiceReference ref = m_context.getServiceReference(
+                    PackageAdmin.class.getName());
+                if (ref == null)
+                {
+                    JOptionPane.showMessageDialog(
+                        JOptionPane.getFrameForComponent(BundleListPlugin.this),
+                        "Unable to obtain PackageAdmin service.", "Error",
+                        JOptionPane.ERROR_MESSAGE);
+                    return;
+                }
+
+                PackageAdmin pa = (PackageAdmin) m_context.getService(ref);
+                if (pa == null)
+                {
+                    JOptionPane.showMessageDialog(
+                        JOptionPane.getFrameForComponent(BundleListPlugin.this),
+                        "Unable to obtain PackageAdmin service.", "Error",
+                        JOptionPane.ERROR_MESSAGE);
+                    return;
+                }
+
+                pa.refreshPackages(null);
+            }
+        });
+
+        m_uninstallButton.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent event)
+            {
+                int[] rows = m_bundleTable.getSelectedRows();
+                // We need to uninstall in reverse order, otherwise
+                // the index will get messed up.
+                for (int i = rows.length - 1; i >= 0; i--)
+                {
+                    try
+                    {
+                        m_context.getBundles()[rows[i]].uninstall();
+                    }
+                    catch (BundleException ex)
+                    {
+                        JOptionPane.showMessageDialog(
+                            JOptionPane.getFrameForComponent(BundleListPlugin.this),
+                            ex.getMessage(), "Error",
+                            JOptionPane.ERROR_MESSAGE);
+                    }
+                }
+            }
+        });
+    }
+
+    private class SimpleTableModel extends AbstractTableModel
+    {
+        public int getRowCount()
+        {
+            return (m_context.getBundles() == null)
+                ? 0 : m_context.getBundles().length;
+        }
+
+        public int getColumnCount()
+        {
+            return 3;
+        }
+
+        public String getColumnName(int column)
+        {
+            if (column == 0)
+            {
+                return "Id";
+            }
+            else if (column == 1)
+            {
+                return "State";
+            }
+            else if (column == 2)
+            {
+                return "Location";
+            }
+            return "";
+        }
+
+        public Class getColumnClass(int column)
+        {
+            if (column == 0)
+            {
+                return Long.class;
+            }
+
+            return String.class;
+        }
+
+        public boolean isCellEditable(int row, int column)
+        {
+            return false;
+        }
+
+        public Object getValueAt(int row, int column)
+        {
+            if (column == 0)
+            {
+                return new Long(m_context.getBundles()[row].getBundleId());
+            }
+            else if (column == 1)
+            {
+                return getStateString(m_context.getBundles()[row].getState());
+            }
+            else if (column == 2)
+            {
+                String name = (String)
+                    m_context.getBundles()[row].getHeaders().get(Constants.BUNDLE_NAME);
+                name = (name == null)
+                    ? m_context.getBundles()[row].getLocation() : name;
+                return name;
+            }
+            return null;
+        }
+
+        public void update()
+        {
+            fireTableDataChanged();
+        }
+
+        private String getStateString(int state)
+        {
+            switch (state)
+            {
+                case Bundle.INSTALLED:
+                    return "Installed";
+                case Bundle.RESOLVED:
+                    return "Resolved";
+                case Bundle.STARTING:
+                    return "Starting";
+                case Bundle.ACTIVE:
+                    return "Active";
+                case Bundle.STOPPING:
+                    return "Stopping";
+            }
+            return "[unknown]";
+        }
+    }
+}
\ No newline at end of file
diff --git a/org.apache.felix.shell.gui.plugin/src/main/java/org/apache/felix/shell/gui/plugin/OBRPlugin.java b/org.apache.felix.shell.gui.plugin/src/main/java/org/apache/felix/shell/gui/plugin/OBRPlugin.java
new file mode 100644
index 0000000..71d9efa
--- /dev/null
+++ b/org.apache.felix.shell.gui.plugin/src/main/java/org/apache/felix/shell/gui/plugin/OBRPlugin.java
@@ -0,0 +1,728 @@
+/*
+ *   Copyright 2006 The Apache Software Foundation
+ *
+ *   Licensed 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.shell.gui.plugin;
+
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.PrintStream;
+import java.net.URL;
+import java.util.*;
+
+import javax.swing.*;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.tree.*;
+
+import org.apache.felix.shell.gui.Plugin;
+import org.osgi.framework.*;
+import org.osgi.service.obr.*;
+
+public class OBRPlugin extends JPanel implements Plugin
+{
+    private static final String DEPLOY_BUTTON = "Deploy";
+    private static final String START_BUTTON = "Deploy & start";
+
+    private BundleContext m_context = null;
+    private ServiceReference m_repoRef = null;
+    private RepositoryAdmin m_repoAdmin = null;
+
+    private SimpleTreeNode m_rootNode = null;
+    private CreateRootRunnable m_createRootRunnable = new CreateRootRunnable();
+    private SetRootRunnable m_setRootRunnable = new SetRootRunnable();
+
+    private JButton m_addRepoButton = null;
+    private JButton m_removeRepoButton = null;
+    private JButton m_refreshRepoButton = null;
+    private JTree m_bundleTree = null;
+    private JButton m_deployButton = null;
+    private JButton m_startButton = null;
+    private JButton m_infoButton = null;
+    private ScrollableOutputArea m_soa = null;
+    private JButton m_clearButton = null;
+
+    private PrintStream m_out = null;
+
+    // Plugin interface methods.
+
+    public String getName()
+    {
+        return "OBR";
+    }
+    
+    public Component getGUI()
+    {
+        return this;
+    }
+    
+    // Implementation.
+    
+    public OBRPlugin(BundleContext context)
+    {
+        m_context = context;
+        m_out = new PrintStream(
+            new OutputAreaStream(
+                m_soa = new ScrollableOutputArea(5, 30)));
+
+        // Create the gui.
+        createUserInterface();
+
+        synchronized (this)
+        {
+            // Listen for registering/unregistering bundle repository services.
+            ServiceListener sl = new ServiceListener() {
+                public void serviceChanged(ServiceEvent event)
+                {
+                    synchronized (OBRPlugin.this)
+                    {
+                        // Ignore additional services if we already have one.
+                        if ((event.getType() == ServiceEvent.REGISTERED)
+                            && (m_repoRef != null))
+                        {
+                            return;
+                        }
+                        // Initialize the service if we don't have one.
+                        else if ((event.getType() == ServiceEvent.REGISTERED)
+                            && (m_repoRef == null))
+                        {
+                            lookupService();
+                        }
+                        // Unget the service if it is unregistering.
+                        else if ((event.getType() == ServiceEvent.UNREGISTERING)
+                            && event.getServiceReference().equals(m_repoRef))
+                        {
+                            m_context.ungetService(m_repoRef);
+                            m_repoRef = null;
+                            m_repoAdmin = null;
+                            // Try to get another service.
+                            lookupService();
+                        }
+                    }
+                }
+            };
+
+            try
+            {
+                m_context.addServiceListener(sl,
+                    "(objectClass=" + RepositoryAdmin.class.getName() + ")");
+            }
+            catch (InvalidSyntaxException ex)
+            {
+                System.err.println("OBRPlugin: " + ex);
+            }
+
+            // Now try to manually initialize the shell service
+            // since one might already be available.
+            lookupService();
+        }
+    }
+
+    private synchronized void lookupService()
+    {
+        if (m_repoAdmin != null)
+        {
+            return;
+        }
+        m_repoRef = m_context.getServiceReference(RepositoryAdmin.class.getName());
+        if (m_repoRef == null)
+        {
+        }
+        else
+        {
+            m_repoAdmin = (RepositoryAdmin) m_context.getService(m_repoRef);
+        }
+
+        // Update the model.
+        initializeRootNode();
+    }
+
+    private void createEventListeners()
+    {
+        // Create action listeners.
+        m_addRepoButton.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent event)
+            {
+                synchronized (OBRPlugin.this)
+                {
+                    if (m_repoAdmin == null)
+                    {
+                        return;
+                    }
+
+                    String s = JOptionPane.showInputDialog(
+                        OBRPlugin.this, "Enter repository URL:");
+
+                    if (s != null)
+                    {
+                        try
+                        {
+                            m_repoAdmin.addRepository(new URL(s));
+                        }
+                        catch (Exception ex)
+                        {
+                            ex.printStackTrace();
+                        }
+                    }
+
+                    // Update the table.
+                    initializeRootNode();
+                }
+            }
+        });
+
+        m_removeRepoButton.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent event)
+            {
+                synchronized (OBRPlugin.this)
+                {
+                    if (m_repoAdmin == null)
+                    {
+                        return;
+                    }
+
+                    TreePath[] paths = m_bundleTree.getSelectionPaths();
+                    for (int i = 0; i < paths.length; i++)
+                    {
+                        SimpleTreeNode node = (SimpleTreeNode) paths[i].getLastPathComponent();
+                        if (node.getObject() instanceof Repository)
+                        {
+                            m_repoAdmin.removeRepository(
+                                ((Repository)
+                                    ((SimpleTreeNode)
+                                        paths[i].getLastPathComponent()).getObject()).getURL());
+                        }
+                    }
+
+                    // Update the table.
+                    initializeRootNode();
+                }
+            }
+        });
+
+        m_refreshRepoButton.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent event)
+            {
+                synchronized (OBRPlugin.this)
+                {
+                    if (m_repoAdmin == null)
+                    {
+                        return;
+                    }
+
+                    TreePath[] paths = m_bundleTree.getSelectionPaths();
+                    for (int i = 0; i < paths.length; i++)
+                    {
+                        SimpleTreeNode node = (SimpleTreeNode) paths[i].getLastPathComponent();
+                        if (node.getObject() instanceof Repository)
+                        {
+                            try
+                            {
+                                // Adding the repository again causes it to be reparsed.
+                                m_repoAdmin.addRepository(
+                                    ((Repository) node.getObject()).getURL());
+                            } catch (Exception ex)
+                            {
+                                ex.printStackTrace();
+                            }
+                        }
+                    }
+
+                    // Update the table.
+                    initializeRootNode();
+                }
+            }
+        });
+
+        ActionListener al = new ActionListener() {
+            public void actionPerformed(ActionEvent event)
+            {
+                boolean start = event.getActionCommand().equals(START_BUTTON);
+
+                synchronized (OBRPlugin.this)
+                {
+                    if (m_repoAdmin == null)
+                    {
+                        return;
+                    }
+
+                    Resolver resolver = m_repoAdmin.resolver();
+                    TreePath[] paths = m_bundleTree.getSelectionPaths();
+                    for (int i = 0; i < paths.length; i++)
+                    {
+                        SimpleTreeNode node = (SimpleTreeNode) paths[i].getLastPathComponent();
+                        if (node.getObject() instanceof Resource)
+                        {
+                            resolver.add((Resource) node.getObject());
+                        }
+                    }
+
+                    if ((resolver.getAddedResources() != null) &&
+                        (resolver.getAddedResources().length > 0))
+                    {
+                        if (resolver.resolve())
+                        {
+                            m_out.println("Target resource(s):");
+                            printUnderline(m_out, 19);
+                            Resource[] resources = resolver.getAddedResources();
+                            for (int resIdx = 0; (resources != null) && (resIdx < resources.length); resIdx++)
+                            {
+                                m_out.println("   " + resources[resIdx].getPresentationName()
+                                    + " (" + resources[resIdx].getVersion() + ")");
+                            }
+                            resources = resolver.getRequiredResources();
+                            if ((resources != null) && (resources.length > 0))
+                            {
+                                m_out.println("\nRequired resource(s):");
+                                printUnderline(m_out, 21);
+                                for (int resIdx = 0; resIdx < resources.length; resIdx++)
+                                {
+                                    m_out.println("   " + resources[resIdx].getPresentationName()
+                                        + " (" + resources[resIdx].getVersion() + ")");
+                                }
+                            }
+                            resources = resolver.getOptionalResources();
+                            if ((resources != null) && (resources.length > 0))
+                            {
+                                m_out.println("\nOptional resource(s):");
+                                printUnderline(m_out, 21);
+                                for (int resIdx = 0; resIdx < resources.length; resIdx++)
+                                {
+                                    m_out.println("   " + resources[resIdx].getPresentationName()
+                                        + " (" + resources[resIdx].getVersion() + ")");
+                                }
+                            }
+
+                            try
+                            {
+                                m_out.print("\nDeploying...");
+                                resolver.deploy(start);
+                                m_out.println("done.");
+                            }
+                            catch (IllegalStateException ex)
+                            {
+                                m_out.println(ex);
+                            }
+                        }
+                        else
+                        {
+                            Requirement[] reqs = resolver.getUnsatisfiedRequirements();
+                            if ((reqs != null) && (reqs.length > 0))
+                            {
+                                m_out.println("Unsatisfied requirement(s):");
+                                printUnderline(m_out, 27);
+                                for (int reqIdx = 0; reqIdx < reqs.length; reqIdx++)
+                                {
+                                    m_out.println("   " + reqs[reqIdx].getFilter());
+                                    Resource[] resources = resolver.getResources(reqs[reqIdx]);
+                                    for (int resIdx = 0; resIdx < resources.length; resIdx++)
+                                    {
+                                        m_out.println("      " + resources[resIdx].getPresentationName());
+                                    }
+                                }
+                            }
+                            else
+                            {
+                                m_out.println("Could not resolve targets.");
+                            }
+                        }
+
+                        m_out.println("");
+                    }
+                }
+            }
+        };
+
+        m_deployButton.addActionListener(al);
+        m_startButton.addActionListener(al);
+
+        m_infoButton.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent event)
+            {
+                synchronized (OBRPlugin.this)
+                {
+                    if (m_repoAdmin == null)
+                    {
+                        return;
+                    }
+                    TreePath[] paths = m_bundleTree.getSelectionPaths();
+                    for (int i = 0; i < paths.length; i++)
+                    {
+                        if (i != 0)
+                        {
+                            m_out.println("");
+                        }
+                        printInfo(m_out,
+                            ((SimpleTreeNode) paths[i].getLastPathComponent()).getObject());
+                    }
+                    m_out.println("");
+                }
+            }
+        });
+
+        m_clearButton.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent event)
+            {
+                synchronized (OBRPlugin.this)
+                {
+                    m_soa.setText("");
+                }
+            }
+        });
+
+        m_bundleTree.addTreeSelectionListener(new TreeSelectionListener() {
+            public void valueChanged(TreeSelectionEvent e)
+            {
+                if (m_repoAdmin == null)
+                {
+                    return;
+                }
+                TreePath[] paths = m_bundleTree.getSelectionPaths();
+                boolean repoOnly = true;
+                if (paths != null)
+                {
+                    for (int i = 0; repoOnly && (i < paths.length); i++)
+                    {
+                        SimpleTreeNode node = (SimpleTreeNode) paths[i].getLastPathComponent();
+                        if (!(node.getObject() instanceof Repository))
+                        {
+                            repoOnly = false;
+                        }
+                    }
+                }
+                m_removeRepoButton.setEnabled((paths != null) && repoOnly);
+                m_refreshRepoButton.setEnabled((paths != null) && repoOnly);
+                m_infoButton.setEnabled((paths != null) && (paths.length > 0));
+            }
+        });
+    }
+
+    private void printInfo(PrintStream out, Object obj)
+    {
+        if (obj != null)
+        {
+            if (obj instanceof Repository)
+            {
+                Repository repo = (Repository) obj;
+                out.println(repo.getName());
+                out.println("   URL = " + repo.getURL());
+                out.println("   Modified = " + new Date(repo.getLastModified()));
+            }
+            else if (obj instanceof Resource)
+            {
+                Resource res = (Resource) obj;
+                out.println(res.getPresentationName());
+
+                // Print properties.
+                Map map = res.getProperties();
+                Iterator iter = map.entrySet().iterator();
+                while (iter.hasNext())
+                {
+                    Map.Entry entry = (Map.Entry) iter.next();
+                    out.println("   " + entry.getKey() + " = " + entry.getValue());
+                }
+
+                // Print requirements.
+                Requirement[] reqs = res.getRequirements();
+                for (int i = 0; (reqs != null) && (i < reqs.length); i++)
+                {
+                    if (i == 0)
+                    {
+                        out.println("   requirements:");
+                    }
+                    out.println("      " + reqs[i].getFilter());
+                }
+
+                // Print capabilities.
+                Capability[] caps = res.getCapabilities();
+                for (int i = 0; (caps != null) && (i < caps.length); i++)
+                {
+                    if (i == 0)
+                    {
+                        out.println("   capabilities:");
+                    }
+                    out.println("      " + caps[i].getName() + " = " + caps[i].getProperties());
+                }
+            }
+        }
+    }
+
+    private void createUserInterface()
+    {
+        JSplitPane split = new JSplitPane(
+            JSplitPane.VERTICAL_SPLIT, createTree(), createConsole());
+        split.setResizeWeight(1.0);
+        split.setDividerSize(5);
+        setLayout(new BorderLayout());
+        add(split, BorderLayout.CENTER);
+        createEventListeners();
+    }
+
+    private JPanel createTree()
+    {
+        JPanel panel = new JPanel(new BorderLayout());
+        panel.add(createRepoPanel(), BorderLayout.NORTH);
+        panel.add(createResourcePanel(), BorderLayout.CENTER);
+        return panel;
+    }
+
+    private JPanel createRepoPanel()
+    {
+        JPanel panel = new JPanel();
+        panel.setBorder(BorderFactory.createTitledBorder("Repositories"));
+        panel.add(m_addRepoButton = new JButton("Add"));
+        m_addRepoButton.setMnemonic('A');
+        panel.add(m_removeRepoButton = new JButton("Remove"));
+        m_removeRepoButton.setMnemonic('R');
+        panel.add(m_refreshRepoButton = new JButton("Refresh"));
+        m_refreshRepoButton.setMnemonic('f');
+        m_removeRepoButton.setEnabled(false);
+        m_refreshRepoButton.setEnabled(false);
+        return panel;
+    }
+
+    private JPanel createResourcePanel()
+    {
+        JPanel panel = new JPanel(new BorderLayout());
+        panel.setBorder(BorderFactory.createTitledBorder("Resources"));
+        JScrollPane scroll = null;
+        panel.add(
+            scroll = new JScrollPane(
+                m_bundleTree = new JTree(new SimpleTreeNode(null, null))), BorderLayout.CENTER);
+        panel.add(createButtonPanel(), BorderLayout.SOUTH);
+
+        // Set table scroll pane to reasonable size.
+        scroll.setPreferredSize(new Dimension(100, 100));
+        m_bundleTree.setMinimumSize(new Dimension(0, 0));
+
+        // We don't need to see the root.
+        m_bundleTree.setRootVisible(false);
+        m_bundleTree.setShowsRootHandles(true);
+
+        return panel;
+    }
+
+    private JPanel createButtonPanel()
+    {
+        JPanel panel = new JPanel();
+        panel.add(m_deployButton = new JButton(DEPLOY_BUTTON));
+        panel.add(m_startButton = new JButton(START_BUTTON));
+        panel.add(m_infoButton = new JButton("Info"));
+        m_deployButton.setMnemonic('D');
+        m_startButton.setMnemonic('S');
+        m_infoButton.setMnemonic('I');
+        m_infoButton.setEnabled(false);
+        return panel;
+    }
+
+    private JPanel createConsole()
+    {
+        JPanel panel = new JPanel(new BorderLayout());
+        panel.add(m_soa, BorderLayout.CENTER);
+        panel.add(m_clearButton = new JButton("Clear"), BorderLayout.EAST);
+        m_clearButton.setMnemonic('C');
+
+        return panel;
+    }
+
+    private static void printUnderline(PrintStream out, int length)
+    {
+        for (int i = 0; i < length; i++)
+        {
+            out.print('-');
+        }
+        out.println("");
+    }
+
+    private void initializeRootNode()
+    {
+        synchronized (m_createRootRunnable)
+        {
+            new Thread(m_createRootRunnable).start();
+        }
+    }
+
+    private class CreateRootRunnable implements Runnable
+    {
+        public void run()
+        {
+            synchronized (OBRPlugin.this)
+            {
+                // HACK ALERT: This next if statement is a hack to force
+                // the OBR service to retrieve its repository files on
+                // this thread, rather than the Swing thread. This hack
+                // assumes that this GUI is working with Felix' OBR service,
+                // which defers retrieving repository URLs until needed.
+                if (m_repoAdmin != null)
+                {
+                    m_repoAdmin.listRepositories();
+                }
+
+                // Create the new root node and then set it.
+                m_rootNode = new SimpleTreeNode(null, m_repoAdmin);
+                try
+                {
+                    SwingUtilities.invokeAndWait(m_setRootRunnable);
+                }
+                catch (Exception ex)
+                {
+                    // Ignore.
+                }
+            }
+        }
+    }
+
+    private class SetRootRunnable implements Runnable
+    {
+        public void run()
+        {
+            ((DefaultTreeModel) m_bundleTree.getModel()).setRoot(m_rootNode);
+        }
+    }
+
+    private static class SimpleTreeNode implements TreeNode
+    {
+        private TreeNode m_parent = null;
+        private Object m_obj = null;
+        private TreeNode[] m_children = null;
+        private String m_toString = null;
+
+        public SimpleTreeNode(TreeNode parent, Object obj)
+        {
+            m_parent = parent;
+            m_obj = obj;
+        }
+
+        public Object getObject()
+        {
+            return m_obj;
+        }
+
+        public TreeNode getChildAt(int index)
+        {
+            if (m_children == null)
+            {
+                initialize();
+            }
+
+            if ((m_children != null) && (index >= 0) && (index < m_children.length))
+            {
+                return m_children[index];
+            }
+
+            return null;
+        }
+
+        public int getChildCount()
+        {
+            if (m_children == null)
+            {
+                initialize();
+            }
+            return (m_children == null) ? 0 : m_children.length;
+        }
+
+        public TreeNode getParent()
+        {
+            return m_parent;
+        }
+
+        public int getIndex(TreeNode node)
+        {
+            if (m_children == null)
+            {
+                initialize();
+            }
+            for (int i = 0; (m_children != null) && (i < m_children.length); i++)
+            {
+                if (m_children[i] == node)
+                {
+                    return i;
+                }
+            }
+            return -1;
+        }
+
+        public boolean getAllowsChildren()
+        {
+            return true;
+        }
+
+        public boolean isLeaf()
+        {
+            return (getChildCount() == 0);
+        }
+
+        public Enumeration children()
+        {
+            return null;
+        }
+
+        private void initialize()
+        {
+            // The current node might be the root repository admin.
+            if ((m_obj != null) && (m_obj instanceof RepositoryAdmin))
+            {
+                Object[] objs = ((RepositoryAdmin) m_obj).listRepositories();
+                if (objs != null)
+                {
+                    m_children = new TreeNode[objs.length];
+                    for (int i = 0; i < objs.length; i++)
+                    {
+                        m_children[i] = new SimpleTreeNode(this, objs[i]);
+                    }
+                }
+            }
+            else if (m_obj instanceof Repository)
+            {
+                Object[] objs = ((Repository) m_obj).getResources();
+                if (objs != null)
+                {
+                    m_children = new TreeNode[objs.length];
+                    for (int i = 0; i < objs.length; i++)
+                    {
+                        m_children[i] = new SimpleTreeNode(this, objs[i]);
+                    }
+                }
+            }
+        }
+
+        public String toString()
+        {
+            if (m_toString == null)
+            {
+                if (m_obj instanceof RepositoryAdmin)
+                {
+                    m_toString = "ROOT";
+                }
+                else if (m_obj instanceof Repository)
+                {
+                    m_toString = ((Repository) m_obj).getName();
+                }
+                else if (m_obj instanceof Resource)
+                {
+                    m_toString = ((Resource) m_obj).getPresentationName()
+                        + " (" + ((Resource) m_obj).getVersion() + ")";
+                }
+                else
+                {
+                    m_toString = (m_obj != null) ? m_obj.toString() : "null";
+                }
+            }
+            return m_toString;
+        }
+    }
+}
\ No newline at end of file
diff --git a/org.apache.felix.shell.gui.plugin/src/main/java/org/apache/felix/shell/gui/plugin/OutputAreaStream.java b/org.apache.felix.shell.gui.plugin/src/main/java/org/apache/felix/shell/gui/plugin/OutputAreaStream.java
new file mode 100644
index 0000000..992b673
--- /dev/null
+++ b/org.apache.felix.shell.gui.plugin/src/main/java/org/apache/felix/shell/gui/plugin/OutputAreaStream.java
@@ -0,0 +1,47 @@
+/*
+ *   Copyright 2006 The Apache Software Foundation
+ *
+ *   Licensed 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.shell.gui.plugin;
+
+import java.io.OutputStream;
+
+class OutputAreaStream extends OutputStream
+{
+    private ScrollableOutputArea m_soa;
+
+    public OutputAreaStream(ScrollableOutputArea soa)
+    {
+        m_soa = soa;
+    }
+
+    public void write(byte[] b)
+    {
+        String tmp = new String(b);
+        m_soa.addText(tmp);
+    }
+
+    public void write(byte[] b, int off, int len)
+    {
+        String tmp = new String(b, off, len);
+        m_soa.addText(tmp);
+    }
+
+    public void write(int b)
+    {
+        byte[] ba = { (byte) b };
+        m_soa.addText(new String(ba));
+    }
+}
\ No newline at end of file
diff --git a/org.apache.felix.shell.gui.plugin/src/main/java/org/apache/felix/shell/gui/plugin/ScrollableOutputArea.java b/org.apache.felix.shell.gui.plugin/src/main/java/org/apache/felix/shell/gui/plugin/ScrollableOutputArea.java
new file mode 100644
index 0000000..c454e51
--- /dev/null
+++ b/org.apache.felix.shell.gui.plugin/src/main/java/org/apache/felix/shell/gui/plugin/ScrollableOutputArea.java
@@ -0,0 +1,78 @@
+/*
+ *   Copyright 2006 The Apache Software Foundation
+ *
+ *   Licensed 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.shell.gui.plugin;
+
+import java.awt.Font;
+
+import javax.swing.*;
+
+public class ScrollableOutputArea extends JScrollPane
+{
+    private JTextArea m_textArea = null;
+
+    public ScrollableOutputArea()
+    {
+        super();
+        m_textArea = new JTextArea();
+        initialize();
+    }
+
+    public ScrollableOutputArea(int rows, int columns)
+    {
+        super();
+        m_textArea = new JTextArea(rows, columns);
+        initialize();
+    }
+
+    private void initialize()
+    {
+        setViewportView(m_textArea);
+        m_textArea.setLineWrap(true);
+        m_textArea.setAutoscrolls(true);
+        m_textArea.setEnabled(true);
+        m_textArea.setEditable(false);
+        m_textArea.setFont(new Font("Monospaced", 0, 12));
+        setAutoscrolls(true);
+    }
+
+    public void setText(String s)
+    {
+        m_textArea.setText(s);
+    }
+
+    public void addText(String text)
+    {
+        m_textArea.append(text);
+        if (m_textArea.isDisplayable())
+        {
+            try
+            {
+                m_textArea.setCaretPosition(Integer.MAX_VALUE); // Scroll to end of window
+            }
+            catch (Exception e)
+            {
+                // Just for safety
+            }
+        }
+        validate();
+        JScrollBar sb = getVerticalScrollBar();
+        if ((sb != null) && (sb.isVisible()))
+        {
+            sb.setValue(Integer.MAX_VALUE);
+        }
+    }
+}
\ No newline at end of file
diff --git a/org.apache.felix.shell.gui.plugin/src/main/java/org/apache/felix/shell/gui/plugin/ShellPlugin.java b/org.apache.felix.shell.gui.plugin/src/main/java/org/apache/felix/shell/gui/plugin/ShellPlugin.java
new file mode 100644
index 0000000..aaaba36
--- /dev/null
+++ b/org.apache.felix.shell.gui.plugin/src/main/java/org/apache/felix/shell/gui/plugin/ShellPlugin.java
@@ -0,0 +1,183 @@
+/*
+ *   Copyright 2006 The Apache Software Foundation
+ *
+ *   Licensed 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.shell.gui.plugin;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.io.PrintStream;
+
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+
+import org.apache.felix.shell.ShellService;
+import org.apache.felix.shell.gui.Plugin;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+
+public class ShellPlugin extends JPanel implements Plugin
+{
+    private BundleContext m_context = null;
+    private PrintStream m_out = null;
+
+    private JTextField m_commandField = new JTextField();
+    private String[] m_history = new String[25];
+    private int m_historyCount = -1;
+    private int m_current = 0; // current history counter
+
+    private ScrollableOutputArea m_soa = new ScrollableOutputArea();
+
+    // Plugin interface methods.
+
+    public String getName()
+    {
+        return "Shell";    
+    }
+    
+    public Component getGUI()
+    {
+        return this;
+    }
+    
+    // Implementation.
+    
+    public ShellPlugin(BundleContext context)
+    {
+        m_context = context;
+        m_out = new PrintStream(new OutputAreaStream(m_soa));
+        initialize();
+    }
+
+    private void initialize()
+    {
+        setLayout(new BorderLayout());
+        m_commandField.addKeyListener(new KeyAdapter() {
+            public void keyPressed(KeyEvent event)
+            {
+                commandFieldKeyPressed(event);
+            }
+        });
+        m_commandField.setFont(new java.awt.Font("Monospaced", 0, 12));
+        add(m_commandField, BorderLayout.SOUTH);
+        add(m_soa, BorderLayout.CENTER);
+    }
+
+    void processCommand(String line)
+    {
+        if (line == null)
+        {
+            return;
+        }
+
+        line = line.trim();
+
+        if (line.length() == 0)
+        {
+            return;
+        }
+
+        // Get shell service.
+        ServiceReference ref = m_context.getServiceReference(
+            org.apache.felix.shell.ShellService.class.getName());
+        if (ref == null)
+        {
+            m_out.println("No shell service is available.");
+            return;
+        }
+
+        ShellService shell = (ShellService) m_context.getService(ref);
+
+        // Print the command line in the output window.
+        m_out.println("-> " + line);
+
+        try {
+            shell.executeCommand(line, m_out, m_out);
+        } catch (Exception ex) {
+            m_out.println(ex.toString());
+            ex.printStackTrace(m_out);
+        }
+
+        m_context.ungetService(ref);
+    }
+
+    private void addToHistory(String command)
+    {
+        m_historyCount++;
+        if (m_historyCount >= m_history.length)
+        {
+            m_historyCount = m_history.length - 1;
+            for (int i = 0; i < m_history.length - 1; i++)
+            {
+                m_history[i] = m_history[i + 1];
+            }
+        }
+        m_history[m_historyCount] = new String(command);
+    }
+
+    private String getFromHistory(int num)
+    {
+        if (num < 0)
+        {
+            return ("");
+        }
+        else if (num > m_historyCount)
+        {
+            return("");
+        }
+        return m_history[num];
+    }
+
+    protected void commandFieldKeyPressed(KeyEvent event)
+    {
+        String command = null;
+        int c = event.getKeyCode();
+        // Cursor up.
+        if (c == 38)
+        {
+            m_current--;
+            if (m_current < 0)
+            {
+                m_current = 0;
+            }
+            m_commandField.setText(getFromHistory(m_current));
+            m_commandField.setCaretPosition(m_commandField.getText().length());
+        }
+        // Cursor down.
+        else if (c == 40)
+        {
+            m_current++;
+            if (m_current > m_historyCount)
+            {
+                m_current = m_historyCount + 1;
+            }
+            m_commandField.setText(getFromHistory(m_current));
+            m_commandField.setCaretPosition(m_commandField.getText().length());
+        }
+        else if (c == 10)
+        {
+            command = m_commandField.getText();
+            if (!command.equals(""))
+            {
+                addToHistory(command);
+                m_current = m_historyCount + 1;
+                processCommand(command);
+            }
+            m_commandField.setText("");
+        }
+    }
+}
\ No newline at end of file