changeset 336:a43132536a6a

Allow plugins to add menu items Reviewed-by: rkennke, vanaltj Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2012-May/001571.html
author Omair Majid <omajid@redhat.com>
date Wed, 30 May 2012 12:29:39 -0400
parents b84371fa2745
children 9368bea3c19c
files client/core/src/main/java/com/redhat/thermostat/client/MainView.java client/core/src/main/java/com/redhat/thermostat/client/MainWindowControllerImpl.java client/core/src/main/java/com/redhat/thermostat/client/MenuRegistry.java client/core/src/main/java/com/redhat/thermostat/client/UiFacadeFactoryImpl.java client/core/src/main/java/com/redhat/thermostat/client/osgi/ThermostatActivator.java client/core/src/main/java/com/redhat/thermostat/client/osgi/service/MenuAction.java client/core/src/main/java/com/redhat/thermostat/client/ui/MainWindow.java client/core/src/test/java/com/redhat/thermostat/client/MainWindowControllerImplTest.java client/core/src/test/java/com/redhat/thermostat/client/MenuRegistryTest.java client/core/src/test/java/com/redhat/thermostat/client/ui/MainWindowTest.java
diffstat 10 files changed, 485 insertions(+), 14 deletions(-) [+]
line wrap: on
line diff
--- a/client/core/src/main/java/com/redhat/thermostat/client/MainView.java	Tue May 29 11:47:38 2012 -0400
+++ b/client/core/src/main/java/com/redhat/thermostat/client/MainView.java	Wed May 30 12:29:39 2012 -0400
@@ -38,6 +38,7 @@
 
 import java.awt.Component;
 
+import com.redhat.thermostat.client.osgi.service.MenuAction;
 import com.redhat.thermostat.client.osgi.service.VMContextAction;
 import com.redhat.thermostat.common.ActionListener;
 import com.redhat.thermostat.common.dao.Ref;
@@ -73,5 +74,9 @@
 
     void setSubView(Component view);
 
+    void addMenu(String parentMenuName, MenuAction action);
+
+    void removeMenu(String parentMenuName, MenuAction action);
+
     void registerVMContextAction(VMContextAction action);
 }
--- a/client/core/src/main/java/com/redhat/thermostat/client/MainWindowControllerImpl.java	Tue May 29 11:47:38 2012 -0400
+++ b/client/core/src/main/java/com/redhat/thermostat/client/MainWindowControllerImpl.java	Wed May 30 12:29:39 2012 -0400
@@ -41,8 +41,7 @@
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
-import org.osgi.framework.BundleException;
-
+import com.redhat.thermostat.client.osgi.service.MenuAction;
 import com.redhat.thermostat.client.osgi.service.VMContextAction;
 import com.redhat.thermostat.client.ui.AboutDialog;
 import com.redhat.thermostat.client.ui.AgentConfigurationController;
@@ -83,13 +82,27 @@
     private ApplicationInfo appInfo;
 
     private UiFacadeFactory facadeFactory;
+    private MenuRegistry menuRegistry;
+    private MenuRegistry.MenuListener menuListener = new MenuRegistry.MenuListener() {
+
+        @Override
+        public void removed(String parentMenuName, MenuAction action) {
+            view.removeMenu(parentMenuName, action);
+        }
+
+        @Override
+        public void added(String parentMenuName, MenuAction action) {
+            view.addMenu(parentMenuName, action);
+        }
+    };
 
     private boolean showHistory;
 
     private VmInformationControllerProvider vmInfoControllerProvider;
-    
-    public MainWindowControllerImpl(UiFacadeFactory facadeFactory, MainView view) {
+
+    public MainWindowControllerImpl(UiFacadeFactory facadeFactory, MainView view, MenuRegistry menuRegistry) {
         this.facadeFactory = facadeFactory;
+        this.menuRegistry = menuRegistry;
 
         ApplicationContext ctx = ApplicationContext.getInstance();
         DAOFactory daoFactory = ctx.getDAOFactory();
@@ -110,6 +123,9 @@
         }
         
         updateView();
+
+        menuRegistry.start();
+        menuRegistry.addMenuListener(menuListener);
     }
 
     private class HostsVMsLoaderImpl implements HostsVMsLoader {
@@ -212,6 +228,10 @@
     }
 
     private void shutdownApplication() {
+        menuRegistry.removeMenuListener(menuListener);
+        menuListener = null;
+        menuRegistry.stop();
+
         view.hideMainWindow();
         ApplicationContext.getInstance().getTimerFactory().shutdown();
         shutdownOSGiFramework();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/core/src/main/java/com/redhat/thermostat/client/MenuRegistry.java	Wed May 30 12:29:39 2012 -0400
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+package com.redhat.thermostat.client;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+
+import com.redhat.thermostat.client.osgi.service.MenuAction;
+
+public class MenuRegistry {
+
+    public static interface MenuListener {
+
+        public void added(String parentMenuName, MenuAction action);
+
+        public void removed(String parentMenuName, MenuAction action);
+    }
+
+    public static final String PARENT_MENU = "parentMenu";
+
+    private static final String FILTER = "(&(" + Constants.OBJECTCLASS + "=" + MenuAction.class.getName() + ")(" + PARENT_MENU + "=*))";
+
+    private ServiceTracker menuTracker;
+
+    private Map<String,List<MenuAction>> menus = new HashMap<>();
+    private List<MenuListener> listeners = new CopyOnWriteArrayList<>();
+
+    public MenuRegistry(BundleContext context) throws InvalidSyntaxException {
+        menuTracker = new ServiceTracker(context, FrameworkUtil.createFilter(FILTER), null) {
+            @Override
+            public Object addingService(ServiceReference reference) {
+                MenuAction action = (MenuAction) super.addingService(reference);
+                String parentMenuName = (String) reference.getProperty(PARENT_MENU);
+                menuAdded(parentMenuName, action);
+                return action;
+            }
+
+            @Override
+            public void removedService(ServiceReference reference, Object service) {
+                if (!(service instanceof MenuAction)) {
+                    throw new AssertionError("removing a non-MenuAction service");
+                }
+                String parentMenuName = (String) reference.getProperty(PARENT_MENU);
+                menuRemoved(parentMenuName, (MenuAction)service);
+                super.removedService(reference, service);
+            }
+        };
+    }
+
+    public void start() {
+        menuTracker.open();
+    }
+
+    public void stop() {
+        menuTracker.close();
+    }
+
+    public void addMenuListener(MenuListener listener) {
+        listeners.add(listener);
+
+        for (Entry<String,List<MenuAction>> entry: menus.entrySet()) {
+            for (MenuAction action: entry.getValue()) {
+                listener.added(entry.getKey(), action);
+            }
+        }
+    }
+
+    public void removeMenuListener(MenuListener listener) {
+        listeners.remove(listener);
+    }
+
+    private void menuAdded(String parentMenuName, MenuAction action) {
+        if (!menus.containsKey(parentMenuName)) {
+            menus.put(parentMenuName, new ArrayList<MenuAction>());
+        }
+        List<MenuAction> list = menus.get(parentMenuName);
+        list.add(action);
+        for (MenuListener listener: listeners) {
+            listener.added(parentMenuName, action);
+        }
+    }
+
+    private void menuRemoved(String parentMenuName, MenuAction action) {
+        if (!menus.containsKey(parentMenuName)) {
+            throw new IllegalArgumentException("unknown parent menu name");
+        }
+        List<MenuAction> list = menus.get(parentMenuName);
+        if (!list.contains(action)) {
+            throw new IllegalArgumentException("unknown menu action");
+        }
+
+        list.remove(action);
+        for (MenuListener listener: listeners) {
+            listener.removed(parentMenuName, action);
+        }
+    }
+
+}
--- a/client/core/src/main/java/com/redhat/thermostat/client/UiFacadeFactoryImpl.java	Tue May 29 11:47:38 2012 -0400
+++ b/client/core/src/main/java/com/redhat/thermostat/client/UiFacadeFactoryImpl.java	Wed May 30 12:29:39 2012 -0400
@@ -40,8 +40,6 @@
 import java.util.Collection;
 import java.util.concurrent.CountDownLatch;
 
-import org.osgi.framework.BundleContext;
-
 import com.redhat.thermostat.client.osgi.service.VMContextAction;
 import com.redhat.thermostat.client.osgi.service.VmInformationService;
 import com.redhat.thermostat.client.ui.HostInformationController;
@@ -57,11 +55,17 @@
 
     private Collection<VmInformationService> vmInformationServices = new ArrayList<>();
     private Collection<VMContextAction> contextAction = new ArrayList<>();
-    
+
+    private MenuRegistry menuRegistry;
+
+    public UiFacadeFactoryImpl(MenuRegistry registry) {
+        menuRegistry = registry;
+    }
+
     @Override
     public MainWindowController getMainWindow() {
         MainView mainView = new MainWindow();
-        return new MainWindowControllerImpl(this, mainView);
+        return new MainWindowControllerImpl(this, mainView, menuRegistry);
     }
 
     @Override
--- a/client/core/src/main/java/com/redhat/thermostat/client/osgi/ThermostatActivator.java	Tue May 29 11:47:38 2012 -0400
+++ b/client/core/src/main/java/com/redhat/thermostat/client/osgi/ThermostatActivator.java	Wed May 30 12:29:39 2012 -0400
@@ -43,6 +43,7 @@
 
 import com.redhat.thermostat.client.GUIClientCommand;
 import com.redhat.thermostat.client.Main;
+import com.redhat.thermostat.client.MenuRegistry;
 import com.redhat.thermostat.client.UiFacadeFactory;
 import com.redhat.thermostat.client.UiFacadeFactoryImpl;
 import com.redhat.thermostat.common.cli.CommandRegistry;
@@ -57,7 +58,8 @@
 
     @Override
     public void start(final BundleContext context) throws Exception {
-        UiFacadeFactory uiFacadeFactory = new UiFacadeFactoryImpl();
+        MenuRegistry menuRegistry = new MenuRegistry(context);
+        UiFacadeFactory uiFacadeFactory = new UiFacadeFactoryImpl(menuRegistry);
 
         vmInfoServiceTracker = new VmInformationServiceTracker(context, uiFacadeFactory);
         vmInfoServiceTracker.open();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/core/src/main/java/com/redhat/thermostat/client/osgi/service/MenuAction.java	Wed May 30 12:29:39 2012 -0400
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+package com.redhat.thermostat.client.osgi.service;
+
+/**
+ * Allows plugins to register menu items.
+ * <p>
+ * To register a menu item for for the menu "File" in thermostat client window,
+ * register a service that implements this class with the property
+ * "parentMenu" set to "File".
+ */
+public interface MenuAction {
+
+    /** The string displayed as the menu item name */
+    String getName();
+
+    /** A generic description of the menu item */
+    String getDescription();
+
+    /** Invoked when the user selects this menu item */
+    void execute();
+}
--- a/client/core/src/main/java/com/redhat/thermostat/client/ui/MainWindow.java	Tue May 29 11:47:38 2012 -0400
+++ b/client/core/src/main/java/com/redhat/thermostat/client/ui/MainWindow.java	Wed May 30 12:29:39 2012 -0400
@@ -74,6 +74,7 @@
 import javax.swing.JTextField;
 import javax.swing.JTree;
 import javax.swing.KeyStroke;
+import javax.swing.MenuElement;
 import javax.swing.SwingUtilities;
 import javax.swing.SwingWorker;
 import javax.swing.ToolTipManager;
@@ -95,6 +96,7 @@
 import com.redhat.thermostat.client.HostsVMsLoader;
 import com.redhat.thermostat.client.MainView;
 import com.redhat.thermostat.client.locale.LocaleResources;
+import com.redhat.thermostat.client.osgi.service.MenuAction;
 import com.redhat.thermostat.client.osgi.service.VMContextAction;
 import com.redhat.thermostat.common.ActionListener;
 import com.redhat.thermostat.common.ActionNotifier;
@@ -221,6 +223,7 @@
 
     private static final long serialVersionUID = 5608972421496808177L;
 
+    private final JMenuBar mainMenuBar;
     private JPanel contentArea = null;
 
     private JTextField searchField = null;
@@ -251,6 +254,8 @@
         ToolTipManager.sharedInstance().registerComponent(agentVmTree);
         contentArea = new JPanel(new BorderLayout());
 
+        mainMenuBar = new JMenuBar();
+
         setupMenus();
         setupPanels();
 
@@ -280,7 +285,6 @@
     }
 
     private void setupMenus() {
-        JMenuBar mainMenuBar = new JMenuBar();
 
         JMenu fileMenu = new JMenu(localize(LocaleResources.MENU_FILE));
         fileMenu.getPopupMenu().setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY, 1));
@@ -623,6 +627,92 @@
         });
     }
 
+    @Override
+    public void addMenu(final String parentMenuName, final MenuAction action) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                JMenu parent = null;
+                int mainMenuCount = mainMenuBar.getMenuCount();
+                for (int i = 0; i < mainMenuCount; i++) {
+                    if (mainMenuBar.getMenu(i).getText().equals(parentMenuName)) {
+                        parent = mainMenuBar.getMenu(i);
+                        break;
+                    }
+                }
+                if (parent == null) {
+                    parent = new JMenu(parentMenuName);
+                    mainMenuBar.add(parent);
+                }
+
+                JMenuItem menu = new JMenuItem(action.getName());
+                menu.addActionListener(new java.awt.event.ActionListener() {
+                    @Override
+                    public void actionPerformed(ActionEvent e) {
+                        action.execute();
+                    }
+                });
+                parent.add(menu);
+
+                mainMenuBar.revalidate();
+            }
+        });
+    }
+
+    @Override
+    public void removeMenu(final String parentMenuName, final  MenuAction action) {
+        final String actionName = action.getName();
+        try {
+            new EdtHelper().callAndWait(new Runnable() {
+                @Override
+                public void run() {
+                    MenuElement parent = null;
+                    int mainMenuCount = mainMenuBar.getMenuCount();
+                    for (int i = 0; i < mainMenuCount; i++) {
+                        if (mainMenuBar.getMenu(i).getText().equals(parentMenuName)) {
+                            parent = mainMenuBar.getMenu(i);
+                            break;
+                        }
+                    }
+                    if (parent == null) {
+                        throw new IllegalArgumentException("parent menu not found");
+                    }
+
+                    boolean removed = false;
+                    MenuElement[] menus = parent.getSubElements();
+                    if (menus.length == 1 && (menus[0] instanceof JPopupMenu)) {
+                        parent = menus[0];
+                        menus = parent.getSubElements();
+                    }
+
+                    for (MenuElement menu: menus) {
+                        if (menu instanceof JMenuItem && ((JMenuItem)menu).getText().equals(actionName)) {
+                            if (parent instanceof JPopupMenu) {
+                                ((JPopupMenu)parent).remove((JMenuItem)menu);
+                                removed = true;
+                            }
+                        }
+                    }
+
+                    if (!removed) {
+                        throw new IllegalArgumentException("child menu not found");
+                    }
+
+                    mainMenuBar.revalidate();
+                }
+            });
+        } catch (InterruptedException ie) {
+            Thread.currentThread().interrupt();
+            throw new RuntimeException(ie);
+        } catch (InvocationTargetException roe) {
+            Throwable cause = roe.getCause();
+            if (cause instanceof IllegalArgumentException) {
+                throw (IllegalArgumentException) cause;
+            }
+            throw new RuntimeException(cause);
+        }
+    }
+
     /**
      * Returns null to indicate no Ref is selected
      */
--- a/client/core/src/test/java/com/redhat/thermostat/client/MainWindowControllerImplTest.java	Tue May 29 11:47:38 2012 -0400
+++ b/client/core/src/test/java/com/redhat/thermostat/client/MainWindowControllerImplTest.java	Wed May 30 12:29:39 2012 -0400
@@ -37,6 +37,7 @@
 package com.redhat.thermostat.client;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyString;
@@ -63,6 +64,8 @@
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.BundleException;
 
+import com.redhat.thermostat.client.MenuRegistry.MenuListener;
+import com.redhat.thermostat.client.osgi.service.MenuAction;
 import com.redhat.thermostat.client.osgi.service.VMContextAction;
 import com.redhat.thermostat.client.ui.SummaryController;
 import com.redhat.thermostat.client.ui.SummaryView;
@@ -99,7 +102,9 @@
 
     private VMContextAction action1;
     private VMContextAction action2;
-    
+
+    private MenuListener menuListener;
+
     @BeforeClass
     public static void setUpOnce() {
         // TODO remove when controller uses mocked objects rather than real swing objects
@@ -129,6 +134,10 @@
         ArgumentCaptor<ActionListener> grabListener = ArgumentCaptor.forClass(ActionListener.class);
         doNothing().when(view).addActionListener(grabListener.capture());
 
+        MenuRegistry registry = mock(MenuRegistry.class);
+        ArgumentCaptor<MenuListener> menuListenerCaptor = ArgumentCaptor.forClass(MenuListener.class);
+        doNothing().when(registry).addMenuListener(menuListenerCaptor.capture());
+
         // TODO remove this asap. the main window has a hard dependency on summary controller/view
         ViewFactory viewFactory = mock(ViewFactory.class);
         SummaryView summaryView = mock(SummaryView.class);
@@ -136,9 +145,10 @@
         ApplicationContext.getInstance().setViewFactory(viewFactory);
 
         setUpVMContextActions();
-        
-        controller = new MainWindowControllerImpl(uiFacadeFactory, view);
+
+        controller = new MainWindowControllerImpl(uiFacadeFactory, view, registry);
         l = grabListener.getValue();
+        menuListener = menuListenerCaptor.getValue();
 
     }
 
@@ -347,6 +357,19 @@
         verify(action2, times(0)).execute(any(VmRef.class));
     }
 
+    @Test
+    public void verifyMenuItems() {
+        assertNotNull(menuListener);
+        MenuAction action = mock(MenuAction.class);
+        when(action.getName()).thenReturn("Test1");
+
+        menuListener.added("File", action);
+        verify(view).addMenu("File", action);
+
+        menuListener.removed("File", action);
+        verify(view).removeMenu("File", action);
+    }
+
    @Test
    public void testOSGiFrameworkShutdown() throws BundleException {
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/core/src/test/java/com/redhat/thermostat/client/MenuRegistryTest.java	Wed May 30 12:29:39 2012 -0400
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isA;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+
+import com.redhat.thermostat.client.MenuRegistry.MenuListener;
+import com.redhat.thermostat.client.osgi.service.MenuAction;
+
+public class MenuRegistryTest {
+
+    @Test
+    public void verifyMenuRegistryReactsToMenuActions() throws InvalidSyntaxException {
+        ArgumentCaptor<ServiceListener> serviceListenerCaptor = ArgumentCaptor.forClass(ServiceListener.class);
+        ArgumentCaptor<String> filterCaptor = ArgumentCaptor.forClass(String.class);
+
+        MenuListener menuListener = mock(MenuListener.class);
+        MenuAction menuAction = mock(MenuAction.class);
+
+        BundleContext context = mock(BundleContext.class);
+        doNothing().when(context).addServiceListener(serviceListenerCaptor.capture(), filterCaptor.capture());
+
+        ServiceReference ref = mock(ServiceReference.class);
+        when(ref.getProperty("objectClass")).thenReturn(MenuAction.class.getName());
+        when(ref.getProperty(MenuRegistry.PARENT_MENU)).thenReturn("Test");
+
+        when(context.getService(ref)).thenReturn(menuAction);
+
+        MenuRegistry registry = new MenuRegistry(context);
+        registry.addMenuListener(menuListener);
+        registry.start();
+
+        ServiceListener serviceListener = serviceListenerCaptor.getValue();
+        serviceListener.serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, ref));
+
+        verify(menuListener).added(eq("Test"), isA(MenuAction.class));
+
+        serviceListener.serviceChanged(new ServiceEvent(ServiceEvent.UNREGISTERING, ref));
+
+        verify(menuListener).removed(eq("Test"), isA(MenuAction.class));
+
+    }
+}
--- a/client/core/src/test/java/com/redhat/thermostat/client/ui/MainWindowTest.java	Tue May 29 11:47:38 2012 -0400
+++ b/client/core/src/test/java/com/redhat/thermostat/client/ui/MainWindowTest.java	Wed May 30 12:29:39 2012 -0400
@@ -37,15 +37,23 @@
 package com.redhat.thermostat.client.ui;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.isA;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import javax.swing.AbstractAction;
+
 import net.java.openjdk.cacio.ctc.junit.CacioFESTRunner;
 
 import org.fest.swing.annotation.GUITest;
 import org.fest.swing.edt.FailOnThreadViolationRepaintManager;
 import org.fest.swing.edt.GuiActionRunner;
 import org.fest.swing.edt.GuiTask;
+import org.fest.swing.exception.ComponentLookupException;
 import org.fest.swing.fixture.FrameFixture;
 import org.fest.swing.fixture.JMenuItemFixture;
 import org.fest.swing.fixture.JTextComponentFixture;
@@ -58,6 +66,7 @@
 import org.junit.runner.RunWith;
 
 import com.redhat.thermostat.client.MainView;
+import com.redhat.thermostat.client.osgi.service.MenuAction;
 import com.redhat.thermostat.common.ActionEvent;
 import com.redhat.thermostat.common.ActionListener;
 
@@ -191,7 +200,38 @@
 
         verify(l).actionPerformed(new ActionEvent<MainView.Action>(window, MainView.Action.SWITCH_HISTORY_MODE));
     }
-    
+
+    @Category(GUITest.class)
+    @Test
+    public void addRemoveMenu() {
+        final String MENU_NAME = "Test";
+        MenuAction action = mock(MenuAction.class);
+        when(action.getName()).thenReturn(MENU_NAME);
+
+        JMenuItemFixture menuItem;
+
+        frameFixture.show();
+
+        window.addMenu("File", action);
+
+        menuItem = frameFixture.menuItemWithPath("File", MENU_NAME);
+        assertNotNull(menuItem);
+        menuItem.click();
+
+        verify(action).execute();
+
+        window.removeMenu("File", action);
+
+        try {
+            menuItem = frameFixture.menuItemWithPath("File", MENU_NAME);
+            // should not reach here
+            assertTrue(false);
+        } catch (ComponentLookupException cle) {
+            // expected
+        }
+
+    }
+
     @Category(GUITest.class)
     @Test
     public void testGetHostVMTreeFilter() {