changeset 220:22d3c37b94a0

UI to enable/disable the agent backend from the client Reviewed-by: rkennke Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2012-April/000720.html
author Omair Majid <omajid@redhat.com>
date Thu, 05 Apr 2012 15:45:55 -0400
parents 96b8fc1109af
children 5b50c4cec7ce
files client/src/main/java/com/redhat/thermostat/client/AgentConfigurationSource.java client/src/main/java/com/redhat/thermostat/client/Configuration.java client/src/main/java/com/redhat/thermostat/client/locale/LocaleResources.java client/src/main/java/com/redhat/thermostat/client/ui/AgentConfigurationController.java client/src/main/java/com/redhat/thermostat/client/ui/AgentConfigurationFrame.java client/src/main/java/com/redhat/thermostat/client/ui/AgentConfigurationModel.java client/src/main/java/com/redhat/thermostat/client/ui/AgentConfigurationView.java client/src/main/java/com/redhat/thermostat/client/ui/MainWindow.java client/src/main/resources/com/redhat/thermostat/client/locale/strings.properties client/src/test/java/com/redhat/thermostat/client/ui/AgentConfigurationControllerTest.java
diffstat 10 files changed, 977 insertions(+), 4 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/main/java/com/redhat/thermostat/client/AgentConfigurationSource.java	Thu Apr 05 15:45:55 2012 -0400
@@ -0,0 +1,63 @@
+/*
+ * 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.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class AgentConfigurationSource {
+
+    // FIXME fix this properly
+
+    public List<String> getKnownAgents() {
+        return Arrays.asList(new String[] { "Agent Smith", "Agent Jones", "Agent Brown" });
+    }
+
+    public Map<String, Boolean> getAgentBackends(String agentName) {
+        Map<String, Boolean> fake = new HashMap<>();
+        fake.put("Monitor New JVMs", true);
+        fake.put("Use up all my CPU Cycles", false);
+        return fake;
+    }
+
+    public void updateAgentConfig(String agentName, Map<String, Boolean> newBackendStatus) {
+        // TODO Auto-generated method stub
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/main/java/com/redhat/thermostat/client/Configuration.java	Thu Apr 05 15:45:55 2012 -0400
@@ -0,0 +1,65 @@
+/*
+ * 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 javax.swing.SwingUtilities;
+
+import com.redhat.thermostat.client.ui.AgentConfigurationController;
+import com.redhat.thermostat.client.ui.AgentConfigurationFrame;
+import com.redhat.thermostat.client.ui.AgentConfigurationModel;
+import com.redhat.thermostat.client.ui.AgentConfigurationView;
+
+public class Configuration {
+
+    public void showAgentConfiguration() {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                AgentConfigurationView view = new AgentConfigurationFrame();
+                AgentConfigurationSource agentPrefs = new AgentConfigurationSource();
+                AgentConfigurationModel model = new AgentConfigurationModel(agentPrefs);
+                AgentConfigurationController controller = new AgentConfigurationController(model, view);
+                controller.showView();
+            }
+        });
+    }
+
+    public static void main(String[] args) {
+        new Configuration().showAgentConfiguration();
+    }
+
+}
--- a/client/src/main/java/com/redhat/thermostat/client/locale/LocaleResources.java	Thu Apr 12 10:38:24 2012 -0400
+++ b/client/src/main/java/com/redhat/thermostat/client/locale/LocaleResources.java	Thu Apr 05 15:45:55 2012 -0400
@@ -55,6 +55,8 @@
     MENU_FILE_IMPORT,
     MENU_FILE_EXPORT,
     MENU_FILE_EXIT,
+    MENU_EDIT,
+    MENU_EDIT_CONFIGURE_AGENT,
     MENU_HELP,
     MENU_HELP_ABOUT,
 
@@ -172,7 +174,11 @@
 
     VM_LOADED_CLASSES,
     VM_CLASSES_CHART_REAL_TIME_LABEL,
-    VM_CLASSES_CHART_LOADED_CLASSES_LABEL;
+    VM_CLASSES_CHART_LOADED_CLASSES_LABEL,
+
+    CONFIGURE_AGENT_WINDOW_TITLE,
+    CONFIGURE_AGENT_AGENTS_LIST,
+    CONFIGURE_ENABLE_BACKENDS;
 
     static final String RESOURCE_BUNDLE =
             "com.redhat.thermostat.client.locale.strings";
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/main/java/com/redhat/thermostat/client/ui/AgentConfigurationController.java	Thu Apr 05 15:45:55 2012 -0400
@@ -0,0 +1,116 @@
+/*
+ * 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.ui;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import com.redhat.thermostat.client.ui.AgentConfigurationView.ConfigurationAction;
+import com.redhat.thermostat.common.ActionEvent;
+import com.redhat.thermostat.common.ActionListener;
+
+public class AgentConfigurationController implements ActionListener<ConfigurationAction> {
+
+    private final AgentConfigurationView view;
+    private final AgentConfigurationModel model;
+    private String agentName = null;
+
+    public AgentConfigurationController(AgentConfigurationModel model, AgentConfigurationView view) {
+        this.view = view;
+        this.model = model;
+
+        view.addActionListener(this);
+
+    }
+
+    public void showView() {
+        Collection<String> agents = model.getAgents();
+        agentName = null;
+        for (String agentName: agents) {
+            if (this.agentName == null) {
+                this.agentName = agentName;
+            }
+            view.addAgent(agentName);
+        }
+        view.showDialog();
+        updateViewFromModel();
+    }
+
+    public void hideView() {
+        view.hideDialog();
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent<ConfigurationAction> actionEvent) {
+        switch (actionEvent.getActionId()) {
+            case SWITCH_AGENT:
+                updateModelFromCurrentView();
+                String agentName = view.getSelectedAgent();
+                this.agentName = agentName;
+                updateViewFromModel();
+                break;
+            case CLOSE_ACCEPT:
+                updateModelFromCurrentView();
+                model.saveConfiguration();
+                /* fall through */
+            case CLOSE_CANCEL:
+                hideView();
+                break;
+            default:
+                throw new IllegalArgumentException("unknown event");
+        }
+    }
+
+    private void updateModelFromCurrentView() {
+        Map<String, Boolean> map = view.getBackendStatus();
+        for (Entry<String, Boolean> entry: map.entrySet()) {
+            model.setBackendEnabled(agentName, entry.getKey(), entry.getValue());
+        }
+
+    }
+
+    private void updateViewFromModel() {
+        Map<String, Boolean> map = new HashMap<>();
+        for (String backendName: model.getBackends(agentName)) {
+            map.put(backendName, model.getAgentBackendEnabled(agentName, backendName));
+        }
+        view.setBackendStatus(map);
+
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/main/java/com/redhat/thermostat/client/ui/AgentConfigurationFrame.java	Thu Apr 05 15:45:55 2012 -0400
@@ -0,0 +1,306 @@
+/*
+ * 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.ui;
+
+import static com.redhat.thermostat.client.locale.Translate.localize;
+
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.swing.Box;
+import javax.swing.DefaultListModel;
+import javax.swing.GroupLayout;
+import javax.swing.GroupLayout.Alignment;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.LayoutStyle.ComponentPlacement;
+import javax.swing.SwingUtilities;
+
+import com.redhat.thermostat.client.locale.LocaleResources;
+import com.redhat.thermostat.common.ActionEvent;
+import com.redhat.thermostat.common.ActionListener;
+import javax.swing.JScrollPane;
+import javax.swing.JList;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+
+public class AgentConfigurationFrame extends JFrame implements AgentConfigurationView, java.awt.event.ActionListener, ListSelectionListener {
+
+    private final CopyOnWriteArrayList<ActionListener<ConfigurationAction>> listeners = new CopyOnWriteArrayList<>();
+
+    private final Map<String, JCheckBox> backends = Collections.synchronizedMap(new HashMap<String, JCheckBox>());
+
+    private final JPanel availableBackendsPanel;
+    private final GridBagConstraints availableBackendsPanelContstraints = new GridBagConstraints();
+
+    private final JButton okayButton;
+    private final JButton cancelButton;
+
+    private final JList<String> agentList;
+    private final DefaultListModel<String> listModel;
+
+
+    public AgentConfigurationFrame() {
+        assertInEDT();
+
+        setTitle(localize(LocaleResources.CONFIGURE_AGENT_WINDOW_TITLE));
+
+        JLabel lblEnabledisableBackends = new JLabel(localize(LocaleResources.CONFIGURE_ENABLE_BACKENDS));
+
+        availableBackendsPanel = new JPanel();
+
+        okayButton = new JButton(localize(LocaleResources.BUTTON_OK));
+        okayButton.addActionListener(this);
+
+        cancelButton = new JButton(localize(LocaleResources.BUTTON_CANCEL));
+        cancelButton.addActionListener(this);
+
+        JScrollPane scrollPane = new JScrollPane();
+
+        JLabel lblAgents = new JLabel(localize(LocaleResources.CONFIGURE_AGENT_AGENTS_LIST));
+
+        GroupLayout groupLayout = new GroupLayout(getContentPane());
+        groupLayout.setHorizontalGroup(
+            groupLayout.createParallelGroup(Alignment.LEADING)
+                .addGroup(groupLayout.createSequentialGroup()
+                    .addContainerGap()
+                    .addGroup(groupLayout.createParallelGroup(Alignment.LEADING)
+                        .addGroup(groupLayout.createSequentialGroup()
+                            .addComponent(scrollPane, GroupLayout.PREFERRED_SIZE, 127, GroupLayout.PREFERRED_SIZE)
+                            .addGap(0)
+                            .addGroup(groupLayout.createParallelGroup(Alignment.LEADING)
+                                .addGroup(Alignment.TRAILING, groupLayout.createSequentialGroup()
+                                    .addComponent(cancelButton)
+                                    .addPreferredGap(ComponentPlacement.RELATED)
+                                    .addComponent(okayButton))
+                                .addGroup(groupLayout.createSequentialGroup()
+                                    .addGap(12)
+                                    .addGroup(groupLayout.createParallelGroup(Alignment.LEADING)
+                                        .addGroup(groupLayout.createSequentialGroup()
+                                            .addGap(12)
+                                            .addComponent(availableBackendsPanel, GroupLayout.DEFAULT_SIZE, 540, Short.MAX_VALUE))
+                                        .addComponent(lblEnabledisableBackends)))))
+                        .addComponent(lblAgents))
+                    .addContainerGap())
+        );
+        groupLayout.setVerticalGroup(
+            groupLayout.createParallelGroup(Alignment.TRAILING)
+                .addGroup(groupLayout.createSequentialGroup()
+                    .addGap(6)
+                    .addComponent(lblAgents)
+                    .addPreferredGap(ComponentPlacement.UNRELATED)
+                    .addGroup(groupLayout.createParallelGroup(Alignment.TRAILING)
+                        .addComponent(scrollPane, GroupLayout.DEFAULT_SIZE, 413, Short.MAX_VALUE)
+                        .addGroup(groupLayout.createSequentialGroup()
+                            .addComponent(lblEnabledisableBackends)
+                            .addGap(2)
+                            .addComponent(availableBackendsPanel, GroupLayout.DEFAULT_SIZE, 365, Short.MAX_VALUE)
+                            .addPreferredGap(ComponentPlacement.RELATED)
+                            .addGroup(groupLayout.createParallelGroup(Alignment.BASELINE)
+                                .addComponent(okayButton)
+                                .addComponent(cancelButton))))
+                    .addContainerGap())
+        );
+
+        listModel = new DefaultListModel<String>();
+        agentList = new JList<String>(listModel);
+        agentList.addListSelectionListener(this);
+        scrollPane.setViewportView(agentList);
+
+        availableBackendsPanel.setLayout(new GridBagLayout());
+        getContentPane().setLayout(groupLayout);
+
+        addWindowListener(new WindowAdapter() {
+            @Override
+            public void windowClosing(WindowEvent e) {
+                fireAction(new ActionEvent<>(AgentConfigurationFrame.this, ConfigurationAction.CLOSE_CANCEL));
+            }
+        });
+    }
+
+    private void resetConstraints() {
+        availableBackendsPanelContstraints.gridwidth = 1;
+        availableBackendsPanelContstraints.gridy = 0;
+        availableBackendsPanelContstraints.gridx = 0;
+        availableBackendsPanelContstraints.weightx = 0;
+        availableBackendsPanelContstraints.weighty = 0;
+        availableBackendsPanelContstraints.anchor = GridBagConstraints.LINE_START;
+        availableBackendsPanelContstraints.fill = GridBagConstraints.BOTH;
+    }
+
+
+    @Override
+    public void addActionListener(ActionListener<ConfigurationAction> listener) {
+        listeners.add(listener);
+    }
+
+    @Override
+    public void removeActionListener(ActionListener<ConfigurationAction> listener) {
+        listeners.remove(listener);
+    }
+
+    @Override
+    public void addAgent(final String agentName) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                listModel.addElement(agentName);
+            }
+        });
+    }
+
+    @Override
+    public String getSelectedAgent() {
+        assertInEDT();
+        return agentList.getSelectedValue();
+    }
+
+    @Override
+    public void clearAllAgents() {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                listModel.clear();
+            }
+        });
+    }
+
+
+    @Override
+    public void setBackendStatus(final Map<String, Boolean> backendStatus) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                backends.clear();
+                availableBackendsPanel.removeAll();
+                resetConstraints();
+
+                for (Entry<String, Boolean> entry: backendStatus.entrySet()) {
+                    String backendName = entry.getKey();
+                    boolean checked = entry.getValue();
+                    JCheckBox checkBox = new JCheckBox(backendName);
+                    checkBox.setSelected(checked);
+                    checkBox.setActionCommand(backendName);
+                    checkBox.addActionListener(AgentConfigurationFrame.this);
+                    backends.put(backendName, checkBox);
+                    availableBackendsPanel.add(checkBox, availableBackendsPanelContstraints);
+                    availableBackendsPanelContstraints.gridy++;
+                }
+                availableBackendsPanelContstraints.weighty = 1.0;
+                availableBackendsPanelContstraints.weightx = 1.0;
+                availableBackendsPanelContstraints.fill = GridBagConstraints.BOTH;
+                availableBackendsPanel.add(Box.createGlue(), availableBackendsPanelContstraints);
+                AgentConfigurationFrame.this.revalidate();
+            }
+        });
+    }
+
+    @Override
+    public Map<String, Boolean> getBackendStatus() {
+        assertInEDT();
+
+        Map<String,Boolean> latestUserSpecified = new HashMap<>();
+        for (Entry<String, JCheckBox> entry: backends.entrySet()) {
+            latestUserSpecified.put(entry.getKey(), entry.getValue().isSelected());
+        }
+        return latestUserSpecified;
+    }
+
+    @Override
+    public void showDialog() {
+        assertInEDT();
+
+        pack();
+        setVisible(true);
+
+        agentList.setSelectedIndex(0);
+    }
+
+    @Override
+    public void hideDialog() {
+        assertInEDT();
+
+        setVisible(false);
+        dispose();
+    }
+
+    @Override
+    public void actionPerformed(java.awt.event.ActionEvent e) {
+        Object source = e.getSource();
+        if (source == okayButton) {
+            fireAction(new ActionEvent<>(this, ConfigurationAction.CLOSE_ACCEPT));
+        } else if (source == cancelButton) {
+            fireAction(new ActionEvent<>(this, ConfigurationAction.CLOSE_CANCEL));
+        }
+    }
+
+    @Override
+    public void valueChanged(ListSelectionEvent e) {
+        if (e.getSource() == agentList) {
+            if (e.getValueIsAdjusting()) {
+                return;
+            }
+            fireAction(new ActionEvent<>(this, ConfigurationAction.SWITCH_AGENT));
+        } else {
+            throw new IllegalStateException("unknown trigger");
+        }
+    }
+
+    private void fireAction(ActionEvent<ConfigurationAction> actionEvent) {
+        for (ActionListener<ConfigurationAction> l: listeners) {
+            l.actionPerformed(actionEvent);
+        }
+    }
+
+    private static void assertInEDT() {
+        if (!SwingUtilities.isEventDispatchThread()) {
+            throw new IllegalStateException("must be called from within the swing EDT");
+        }
+    }
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/main/java/com/redhat/thermostat/client/ui/AgentConfigurationModel.java	Thu Apr 05 15:45:55 2012 -0400
@@ -0,0 +1,92 @@
+/*
+ * 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.ui;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import com.redhat.thermostat.client.AgentConfigurationSource;
+
+/**
+ * This model sits between the current view and the remote model, and allows
+ * us to make changes and later throw them away.
+ */
+public class AgentConfigurationModel {
+
+    private final AgentConfigurationSource remoteConfiguration;
+
+    private final List<String> knownAgents;
+    private Map<String, Map<String, Boolean>> enabledBackends;
+
+    public AgentConfigurationModel(AgentConfigurationSource configSource) {
+        this.remoteConfiguration = configSource;
+
+        knownAgents = new ArrayList<>(remoteConfiguration.getKnownAgents());
+        enabledBackends = new HashMap<>();
+        for (String agent: knownAgents) {
+            enabledBackends.put(agent, new HashMap<String, Boolean>(remoteConfiguration.getAgentBackends(agent)));
+        }
+    }
+
+    public Collection<String> getAgents() {
+        return Collections.unmodifiableList(knownAgents);
+    }
+
+    public Collection<String> getBackends(String agentName) {
+        return Collections.unmodifiableSet(enabledBackends.get(agentName).keySet());
+    }
+
+    public void setBackendEnabled(String agentName, String backendName, boolean enabled) {
+        enabledBackends.get(agentName).put(backendName, enabled);
+    }
+
+    public boolean getAgentBackendEnabled(String agentName, String backendName) {
+        return enabledBackends.get(agentName).get(backendName);
+    }
+
+    public void saveConfiguration() {
+        for (Entry<String, Map<String, Boolean>> entry: enabledBackends.entrySet()) {
+            remoteConfiguration.updateAgentConfig(entry.getKey(), entry.getValue());
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/main/java/com/redhat/thermostat/client/ui/AgentConfigurationView.java	Thu Apr 05 15:45:55 2012 -0400
@@ -0,0 +1,69 @@
+/*
+ * 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.ui;
+
+import java.util.Map;
+
+import com.redhat.thermostat.common.ActionListener;
+
+public interface AgentConfigurationView {
+
+    enum ConfigurationAction {
+        SWITCH_AGENT,
+        CLOSE_ACCEPT,
+        CLOSE_CANCEL,
+    }
+
+    void showDialog();
+
+    void hideDialog();
+
+    void addActionListener(ActionListener<ConfigurationAction> listener);
+
+    void removeActionListener(ActionListener<ConfigurationAction> listener);
+
+    void addAgent(String agentName);
+
+    String getSelectedAgent();
+
+    void clearAllAgents();
+
+    void setBackendStatus(Map<String,Boolean> agentConfiguration);
+
+    Map<String,Boolean> getBackendStatus();
+
+}
--- a/client/src/main/java/com/redhat/thermostat/client/ui/MainWindow.java	Thu Apr 12 10:38:24 2012 -0400
+++ b/client/src/main/java/com/redhat/thermostat/client/ui/MainWindow.java	Thu Apr 05 15:45:55 2012 -0400
@@ -43,6 +43,7 @@
 import java.awt.Component;
 import java.awt.Dimension;
 import java.awt.Insets;
+import java.awt.event.ActionEvent;
 import java.awt.event.InputEvent;
 import java.awt.event.KeyEvent;
 import java.awt.event.WindowAdapter;
@@ -84,6 +85,8 @@
 import javax.swing.tree.TreePath;
 import javax.swing.tree.TreeSelectionModel;
 
+import com.redhat.thermostat.client.Configuration;
+import com.redhat.thermostat.client.AgentConfigurationSource;
 import com.redhat.thermostat.client.ApplicationInfo;
 import com.redhat.thermostat.client.HostsVMsLoader;
 import com.redhat.thermostat.client.MainView;
@@ -223,17 +226,17 @@
 
     private final ShutdownClient shutdownAction;
 
-    private ApplicationInfo appInfo; 
+    private ApplicationInfo appInfo;
 
     private ActionNotifier<Action> actionNotifier = new ActionNotifier<>(this);
 
-    private final DefaultMutableTreeNode publishedRoot = 
+    private final DefaultMutableTreeNode publishedRoot =
             new DefaultMutableTreeNode(localize(LocaleResources.MAIN_WINDOW_TREE_ROOT_NAME));
     private final DefaultTreeModel publishedTreeModel = new DefaultTreeModel(publishedRoot);
 
     public MainWindow(UiFacadeFactory facadeFactory) {
         super();
-        
+
         appInfo = new ApplicationInfo();
         setTitle(appInfo.getName());
 
@@ -292,6 +295,18 @@
         fileExitMenu.addActionListener(shutdownAction);
         fileMenu.add(fileExitMenu);
 
+        JMenu editMenu = new JMenu(localize(LocaleResources.MENU_EDIT));
+        mainMenuBar.add(editMenu);
+
+        JMenuItem configureAgentMenuItem = new JMenuItem(localize(LocaleResources.MENU_EDIT_CONFIGURE_AGENT));
+        configureAgentMenuItem.addActionListener(new java.awt.event.ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                new Configuration().showAgentConfiguration();
+            }
+        });
+        editMenu.add(configureAgentMenuItem);
+
         JMenu helpMenu = new JMenu(localize(LocaleResources.MENU_HELP));
         helpMenu.getPopupMenu().setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY, 1));
         mainMenuBar.add(helpMenu);
@@ -499,6 +514,7 @@
         }
     }
 
+    @Override
     public void addActionListener(ActionListener<Action> l) {
         actionNotifier.addActionListener(l);
     }
@@ -511,6 +527,7 @@
         actionNotifier.fireAction(action);
     }
 
+    @Override
     public void updateTree(String filter, HostsVMsLoader hostsVMsLoader) {
         BackgroundTreeModelWorker worker = new BackgroundTreeModelWorker(publishedTreeModel, publishedRoot, filter, hostsVMsLoader);
         worker.execute();
--- a/client/src/main/resources/com/redhat/thermostat/client/locale/strings.properties	Thu Apr 12 10:38:24 2012 -0400
+++ b/client/src/main/resources/com/redhat/thermostat/client/locale/strings.properties	Thu Apr 05 15:45:55 2012 -0400
@@ -15,6 +15,8 @@
 MENU_FILE_IMPORT = Import
 MENU_FILE_EXPORT = Export
 MENU_FILE_EXIT = Exit
+MENU_EDIT = Edit
+MENU_EDIT_CONFIGURE_AGENT = Configure Agent...
 MENU_HELP = Help
 MENU_HELP_ABOUT = About
 
@@ -135,3 +137,7 @@
 VM_CLASSES_CHART_REAL_TIME_LABEL = Time
 VM_CLASSES_CHART_LOADED_CLASSES_LABEL = Number of loaded classes
 VM_INFO_TAB_CLASSES = Classes
+
+CONFIGURE_AGENT_WINDOW_TITLE = Configure Agent Backends
+CONFIGURE_AGENT_AGENTS_LIST = Agents
+CONFIGURE_ENABLE_BACKENDS = Enable/Disable Backends
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/test/java/com/redhat/thermostat/client/ui/AgentConfigurationControllerTest.java	Thu Apr 05 15:45:55 2012 -0400
@@ -0,0 +1,233 @@
+/*
+ * 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.ui;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import com.redhat.thermostat.client.AgentConfigurationSource;
+import com.redhat.thermostat.client.ui.AgentConfigurationView.ConfigurationAction;
+import com.redhat.thermostat.common.ActionEvent;
+import com.redhat.thermostat.common.ActionListener;
+import com.redhat.thermostat.common.Timer;
+import com.redhat.thermostat.common.TimerFactory;
+import com.redhat.thermostat.common.appctx.ApplicationContext;
+import com.redhat.thermostat.common.appctx.ApplicationContextUtil;
+
+public class AgentConfigurationControllerTest {
+
+    @Before
+    public void setUp() {
+        ApplicationContextUtil.resetApplicationContext();
+
+        /*
+         * Set up a mock timer factory that always executes actions synchronously on start();
+         */
+        TimerFactory tf = mock(TimerFactory.class);
+        Timer timer = mock(Timer.class);
+        final Runnable[] runnable = new Runnable[1];
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                runnable[0] = (Runnable) invocation.getArguments()[0];
+                return null;
+            }
+
+        }).when(timer).setAction(any(Runnable.class));
+
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                runnable[0].run();
+                return null;
+            }
+
+        }).when(timer).start();
+        when(tf.createTimer()).thenReturn(timer);
+        ApplicationContext.getInstance().setTimerFactory(tf);
+
+    }
+
+    @After
+    public void tearDown() {
+        ApplicationContextUtil.resetApplicationContext();
+    }
+
+    @Test
+    public void testAddingEnabledBackends() {
+        AgentConfigurationView view = mock(AgentConfigurationView.class);
+        AgentConfigurationModel model = mock(AgentConfigurationModel.class);
+        when(model.getAgents()).thenReturn(Arrays.asList(new String[]{"agent1"}));
+        List<String> backends = Arrays.asList(new String[] {"backend1", "backend2"});
+        when(model.getBackends(any(String.class))).thenReturn(backends);
+        when(model.getAgentBackendEnabled(any(String.class), any(String.class))).thenReturn(true);
+
+        Map<String,Boolean> expected = new HashMap<>();
+        expected.put("backend1", true);
+        expected.put("backend2", true);
+
+        AgentConfigurationController controller = new AgentConfigurationController(model, view);
+        controller.showView();
+        controller.hideView();
+
+        verify(view).addAgent("agent1");
+        verify(view).setBackendStatus(eq(expected));
+        verify(view).showDialog();
+        verify(view).hideDialog();
+    }
+
+    @Test
+    public void testAddingDisabledBackends() {
+        AgentConfigurationView view = mock(AgentConfigurationView.class);
+        AgentConfigurationModel model = mock(AgentConfigurationModel.class);
+        when(model.getAgents()).thenReturn(Arrays.asList(new String[]{"agent1"}));
+        List<String> backends = Arrays.asList(new String[] {"backend1",});
+        when(model.getBackends(any(String.class))).thenReturn(backends);
+        when(model.getAgentBackendEnabled(any(String.class), any(String.class))).thenReturn(false);
+
+        Map<String,Boolean> expected = new HashMap<>();
+        expected.put("backend1", false);
+
+        AgentConfigurationController controller = new AgentConfigurationController(model, view);
+        controller.showView();
+        controller.hideView();
+
+        verify(view).addAgent("agent1");
+        verify(view).setBackendStatus(eq(expected));
+        verify(view).showDialog();
+        verify(view).hideDialog();
+    }
+
+    /**
+     * Verify that the accepting the changes signals the controller
+     */
+    @Test
+    public void testViewEditedBackends() {
+        final ActionListener<ConfigurationAction>[] listeners = (ActionListener<ConfigurationAction>[]) new ActionListener<?>[1];
+        AgentConfigurationView view = mock(AgentConfigurationView.class);
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                listeners[0] = (ActionListener<ConfigurationAction>) invocation.getArguments()[0];
+                return null;
+            }
+        }).when(view).addActionListener(any(ActionListener.class));
+
+
+        AgentConfigurationModel model = mock(AgentConfigurationModel.class);
+        when(model.getAgents()).thenReturn(Arrays.asList(new String[]{"agent1"}));
+        List<String> backends = Arrays.asList(new String[] {"backend1"});
+        when(model.getBackends(any(String.class))).thenReturn(backends);
+        when(model.getAgentBackendEnabled(any(String.class), any(String.class))).thenReturn(true);
+
+        Map<String,Boolean> expected = new HashMap<>();
+        expected.put("backend1", true);
+
+        AgentConfigurationController controller = new AgentConfigurationController(model, view);
+        controller.showView();
+
+        listeners[0].actionPerformed(new ActionEvent<ConfigurationAction>(view, ConfigurationAction.CLOSE_ACCEPT));
+
+        controller.hideView();
+
+        InOrder inOrder = inOrder(view);
+
+        inOrder.verify(view).addAgent("agent1");
+        inOrder.verify(view).setBackendStatus(eq(expected));
+        inOrder.verify(view).getBackendStatus();
+
+        verify(model).saveConfiguration();
+    }
+
+    /**
+     * Verify that controller handles cancel properly
+     */
+    @Test
+    public void testViewCancelEditingBackends() {
+        final ActionListener<ConfigurationAction>[] listeners = (ActionListener<ConfigurationAction>[]) new ActionListener<?>[1];
+        AgentConfigurationView view = mock(AgentConfigurationView.class);
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                listeners[0] = (ActionListener<ConfigurationAction>) invocation.getArguments()[0];
+                return null;
+            }
+        }).when(view).addActionListener(any(ActionListener.class));
+
+
+        AgentConfigurationModel model = mock(AgentConfigurationModel.class);
+        when(model.getAgents()).thenReturn(Arrays.asList(new String[]{"agent1"}));
+        List<String> backends = Arrays.asList(new String[] {"backend1"});
+        when(model.getBackends(any(String.class))).thenReturn(backends);
+        when(model.getAgentBackendEnabled(any(String.class), any(String.class))).thenReturn(true);
+
+        Map<String,Boolean> expectedConfig = new HashMap<>();
+        expectedConfig.put("backend1", true);
+
+        AgentConfigurationController controller = new AgentConfigurationController(model, view);
+        controller.showView();
+
+        listeners[0].actionPerformed(new ActionEvent<ConfigurationAction>(view, ConfigurationAction.CLOSE_CANCEL));
+
+        controller.hideView();
+
+        verify(view).addAgent("agent1");
+        verify(view).setBackendStatus(eq(expectedConfig));
+        verify(view, never()).getBackendStatus();
+
+        verify(model, never()).saveConfiguration();
+    }
+}