changeset 2433:2689d87205f2

Add "shifter-style" rules loader to SwingVmBytemanView This patch modifies the Rules tab of the Byteman plugin by introducing a shifter-style UI for injecting and unloading rules. There now exists a seperate textarea for unloaded and injected rules, and buttons that can be used to transfer rules between them. Additionally, the ability to load Byteman rules from external files has been added. Reviewed-by: jerboaa Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2016-August/020478.html
author Alex Macdonald <almacdon@redhat.com>
date Fri, 26 Aug 2016 14:08:00 -0400
parents b36e89aa7070
children 46b76de8b058
files vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/LocaleResources.java vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanView.java vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/VmBytemanInformationController.java vm-byteman/client-swing/src/main/resources/com/redhat/thermostat/vm/byteman/client/swing/internal/strings.properties vm-byteman/client-swing/src/test/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanViewTest.java
diffstat 5 files changed, 370 insertions(+), 48 deletions(-) [+]
line wrap: on
line diff
--- a/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/LocaleResources.java	Fri Aug 26 11:48:51 2016 -0400
+++ b/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/LocaleResources.java	Fri Aug 26 14:08:00 2016 -0400
@@ -52,6 +52,9 @@
     RULE_EMPTY,
     NO_RULES_LOADED,
     NO_METRICS_AVAILABLE,
+    LABEL_LOCAL_RULE,
+    LABEL_INJECTED_RULE,
+    IMPORT_RULE,
     ;
     
     static final String RESOURCE_BUNDLE = LocaleResources.class.getPackage().getName() + ".strings";
--- a/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanView.java	Fri Aug 26 11:48:51 2016 -0400
+++ b/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanView.java	Fri Aug 26 14:08:00 2016 -0400
@@ -41,11 +41,18 @@
 import java.awt.ComponentOrientation;
 import java.awt.Cursor;
 import java.awt.FlowLayout;
+import java.awt.Graphics;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
 import java.awt.Insets;
 import java.beans.PropertyChangeEvent;
 import java.beans.PropertyChangeListener;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
 import java.text.DateFormat;
 import java.util.ArrayList;
 import java.util.Date;
@@ -58,6 +65,7 @@
 import java.util.logging.Logger;
 
 import javax.swing.JButton;
+import javax.swing.JFileChooser;
 import javax.swing.JLabel;
 import javax.swing.JOptionPane;
 import javax.swing.JPanel;
@@ -66,12 +74,15 @@
 import javax.swing.JTextArea;
 import javax.swing.JTextField;
 import javax.swing.JToggleButton;
+import javax.swing.JSplitPane;
 import javax.swing.SwingUtilities;
 import javax.swing.SwingWorker;
 import javax.swing.border.LineBorder;
 import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
 import javax.swing.event.DocumentEvent;
+import javax.swing.plaf.basic.BasicSplitPaneDivider;
+import javax.swing.plaf.basic.BasicSplitPaneUI;
 
 import com.redhat.thermostat.client.swing.IconResource;
 import com.redhat.thermostat.client.swing.SwingComponent;
@@ -113,17 +124,26 @@
     private static final Translate<LocaleResources> t = LocaleResources.createLocalizer();
     private static final Icon START_ICON = IconResource.SAMPLE.getIcon();
     private static final Icon STOP_ICON = new FontAwesomeIcon('\uf28e', START_ICON.getIconHeight());
+    private static final Icon ARROW_LEFT = IconResource.ARROW_LEFT.getIcon();
+    private static final Icon ARROW_RIGHT = IconResource.ARROW_RIGHT.getIcon();
     private static final String EMPTY_STR = "";
     
     static final String NO_METRICS_AVAILABLE = t.localize(LocaleResources.NO_METRICS_AVAILABLE).getContents();
     
     // Names for buttons used in testing
     static final String TOGGLE_BUTTON_NAME = "TOGGLE_RULE_BUTTON";
-    static final String RULES_TEXT_NAME = "RULES_TEXT";
+    static final String RULES_INJECTED_TEXT_NAME = "RULES_INJECTED_TEXT";
+    static final String RULES_UNLOADED_TEXT_NAME = "RULES_UNLOADED_TEXT";
     static final String METRICS_TEXT_NAME = "METRICS_TEXT";
     
+    private String injectedRuleContent;
+    private String unloadedRuleContent;
+    private boolean generateRuleToggle;
     private final JTextArea metricsText;
-    private final JTextArea rulesText;
+    private final JTextArea unloadedRulesText;
+    private final JTextArea injectedRulesText;
+    private final JButton injectRuleButton;
+    private final JButton unloadRuleButton;
     private JPanel graphMainPanel;
     private ChartPanel graphPanel;
     private JFreeChart graph;
@@ -175,29 +195,119 @@
         final double yWeightRow1 = 0.90;
         final double yWeightRow2 = 0.05;
         final double xWeightFullWidth = 1.0;
+        final double halfWeight = 0.5;
         final Insets paddingInsets = new Insets(5, 5, 5, 5);
 
         // Rules tab
         final JPanel rulesPanel = new JPanel();
         rulesPanel.setLayout(new GridBagLayout());
-        rulesText = new ThermostatTextArea(EMPTY_STR);
-        rulesText.setName(RULES_TEXT_NAME);
-        rulesText.setMargin(paddingInsets);
-        rulesText.setBackground(Color.WHITE);
-        rulesText.setCursor(new Cursor(Cursor.TEXT_CURSOR));
-        rulesText.setBorder(new LineBorder(Color.BLACK));
+
+        // Label Descriptors
+        JLabel localLabel = new JLabel(t.localize(LocaleResources.LABEL_LOCAL_RULE).getContents());
         GridBagConstraints cRules = new GridBagConstraints();
+        cRules.gridx = 0;
+        cRules.gridy = 0;
+        cRules.anchor = GridBagConstraints.LINE_START;
+        cRules.insets = paddingInsets;
+        rulesPanel.add(localLabel, cRules);
+        JLabel injectLabel = new JLabel(t.localize(LocaleResources.LABEL_INJECTED_RULE).getContents());
+        cRules = new GridBagConstraints();
+        cRules.gridx = 1;
+        cRules.gridy = 0;
+        cRules.anchor = GridBagConstraints.LINE_END;
+        cRules.insets = paddingInsets;
+        rulesPanel.add(injectLabel, cRules);
+
+        // Unloaded Rules TextArea
+        unloadedRulesText = new ThermostatTextArea(EMPTY_STR);
+        unloadedRulesText.setName(RULES_UNLOADED_TEXT_NAME);
+        unloadedRulesText.setMargin(paddingInsets);
+        unloadedRulesText.setBackground(Color.WHITE);
+        unloadedRulesText.setCursor(new Cursor(Cursor.TEXT_CURSOR));
+        unloadedRulesText.setBorder(new LineBorder(Color.BLACK));
+        unloadedRulesText.setText(t.localize(LocaleResources.NO_RULES_LOADED).getContents());
+        JScrollPane unloadedPane = new ThermostatScrollPane(unloadedRulesText);
+        unloadedPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
+
+        // Injected Rules TextArea
+        injectedRulesText = new ThermostatTextArea(EMPTY_STR);
+        injectedRulesText.setName(RULES_INJECTED_TEXT_NAME);
+        injectedRulesText.setMargin(paddingInsets);
+        injectedRulesText.setBackground(Color.WHITE);
+        injectedRulesText.setCursor(new Cursor(Cursor.TEXT_CURSOR));
+        injectedRulesText.setBorder(new LineBorder(Color.BLACK));
+        injectedRulesText.setEditable(false);
+        injectedRulesText.setText(t.localize(LocaleResources.NO_RULES_LOADED).getContents());
+        JScrollPane injectedPane = new ThermostatScrollPane(injectedRulesText);
+        injectedPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
+
+        // Inject & Unload Buttons
+        injectRuleButton = new JButton(ARROW_RIGHT);
+        injectRuleButton.addActionListener(new java.awt.event.ActionListener() {
+            @Override
+            public void actionPerformed(java.awt.event.ActionEvent e) {
+                fireInjectAction(InjectAction.INJECT_RULE);
+            }
+        });
+        unloadRuleButton = new JButton(ARROW_LEFT);
+        unloadRuleButton.setEnabled(false);
+        unloadRuleButton.addActionListener(new java.awt.event.ActionListener() {
+            @Override
+            public void actionPerformed(java.awt.event.ActionEvent e) {
+                fireInjectAction(InjectAction.UNLOAD_RULE);
+            }
+        });
+
+        // Split Pane and Divider
+        JSplitPane splitPane = new JSplitPane();
+        splitPane.setLeftComponent(unloadedPane);
+        splitPane.setRightComponent(injectedPane);
+        splitPane.setResizeWeight(halfWeight);
+        BasicSplitPaneUI ui = new BasicSplitPaneUI() {
+            public BasicSplitPaneDivider createDefaultDivider() {
+                return new BasicSplitPaneDivider(this) {
+                    @Override
+                    public void paint(Graphics g) {
+                        g.setColor(rulesPanel.getBackground());
+                        g.fillRect(0, 0, getSize().width, getSize().height);
+                        super.paint(g);
+                    }
+                };
+            }
+        };
+        splitPane.setUI(ui);
+        BasicSplitPaneDivider divider = ui.getDivider();
+        divider.setCursor(Cursor.getDefaultCursor());
+        divider.setLayout(new GridBagLayout());
+        divider.setDividerSize((int)(injectRuleButton.getPreferredSize().getWidth())*2);
+        cRules = new GridBagConstraints();
         cRules.fill = GridBagConstraints.BOTH;
         cRules.gridx = 0;
-        cRules.gridy = 0;
+        cRules.gridy = 1;
+        divider.add(unloadRuleButton, cRules);
+        cRules = new GridBagConstraints();
+        cRules.fill = GridBagConstraints.BOTH;
+        cRules.gridx = 1;
+        cRules.gridy = 1;
+        divider.add(injectRuleButton, cRules);
+        cRules = new GridBagConstraints();
+        cRules.weighty = halfWeight;
+        cRules = new GridBagConstraints();
+        cRules.fill = GridBagConstraints.BOTH;
+        cRules.gridx = 0;
+        cRules.gridwidth = 2;
+        cRules.gridy = 1;
         cRules.weighty = yWeightRow0 + yWeightRow1;
         cRules.weightx = xWeightFullWidth;
         cRules.insets = paddingInsets;
-        JScrollPane scrollPane = new ThermostatScrollPane(rulesText);
-        rulesPanel.add(scrollPane, cRules);
+        rulesPanel.add(splitPane, cRules);
+
+        // Import & Generate Rule Buttons
+        cRules = new GridBagConstraints();
         cRules.fill = GridBagConstraints.BOTH;
         cRules.gridx = 0;
-        cRules.gridy = 1;
+        cRules.gridy = 2;
+        cRules.gridwidth = 2;
         cRules.weighty = yWeightRow2;
         cRules.weightx = xWeightFullWidth;
         cRules.insets = paddingInsets;
@@ -213,11 +323,36 @@
             
             @Override
             public void actionPerformed(java.awt.event.ActionEvent e) {
+                generateRuleToggle = true;
                 fireGenerateEvent(GenerateAction.GENERATE_TEMPLATE);
             }
             
         });
         buttonHolder.add(generateRuleButton);
+        JButton importRuleButton = new JButton(t.localize(LocaleResources.IMPORT_RULE).getContents());
+        importRuleButton.addActionListener(new java.awt.event.ActionListener() {
+            @Override
+            public void actionPerformed(java.awt.event.ActionEvent e) {
+                JFileChooser chooser = new JFileChooser();
+                int result = chooser.showOpenDialog(rulesPanel);
+                if (result == JFileChooser.APPROVE_OPTION) {
+                    File file = chooser.getSelectedFile();
+                    try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
+                        unloadedRulesText.setText(EMPTY_STR);
+                        String line = reader.readLine();
+                        while(line != null) {
+                            unloadedRulesText.append(line + "\n");
+                            line = reader.readLine();
+                        }
+                    } catch (FileNotFoundException fnfe) {
+                        fnfe.printStackTrace();
+                    } catch (IOException ioe) {
+                        ioe.printStackTrace();
+                    }
+                }
+            }
+        });
+        buttonHolder.add(importRuleButton);
         buttonHolder.setAlignmentX(Component.RIGHT_ALIGNMENT);
         rulesPanel.add(buttonHolder, cRules);
         
@@ -488,23 +623,37 @@
 
     @Override
     public void setInjectState(final BytemanInjectState state) {
-        final String buttonLabel;
-        if (!viewControlsEnabled) {
-            buttonLabel = t.localize(LocaleResources.INJECT_RULE).getContents();
-        } else if (state == BytemanInjectState.UNLOADING || state == BytemanInjectState.INJECTED) {
-            buttonLabel = t.localize(LocaleResources.UNLOAD_RULE).getContents();
-        } else {
-            buttonLabel = t.localize(LocaleResources.INJECT_RULE).getContents();
-        }
         SwingUtilities.invokeLater(new Runnable() {
             @Override
             public void run() {
+                final String buttonLabel;
+                if (!viewControlsEnabled) {
+                    buttonLabel = t.localize(LocaleResources.INJECT_RULE).getContents();
+                } else if (state == BytemanInjectState.UNLOADING || state == BytemanInjectState.INJECTED) {
+                    buttonLabel = t.localize(LocaleResources.UNLOAD_RULE).getContents();
+                } else {
+                    buttonLabel = t.localize(LocaleResources.INJECT_RULE).getContents();
+                }
+
                 if (!viewControlsEnabled) {
                     toggleButton.setToggleActionState(BytemanInjectState.DISABLED);
                 } else {
                     toggleButton.setToggleActionState(state);
                 }
                 toggleButton.setText(buttonLabel);
+
+                if (state == BytemanInjectState.INJECTED) {
+                    injectedRulesText.setText(unloadedRulesText.getText());
+                    injectRuleButton.setEnabled(false);
+                    unloadRuleButton.setEnabled(true);
+                } else if (state == BytemanInjectState.UNLOADING) {
+                    if (EMPTY_STR.equals(unloadedRulesText.getText().trim())) {
+                        unloadedRulesText.setText(injectedRulesText.getText());
+                    }
+                } else if (state == BytemanInjectState.UNLOADED){
+                    injectRuleButton.setEnabled(true);
+                    unloadRuleButton.setEnabled(false);
+                }
             }
         });
         
@@ -546,7 +695,12 @@
         SwingUtilities.invokeLater(new Runnable() {
             @Override
             public void run() {
-                rulesText.setText(rule);
+                if (generateRuleToggle) {
+                    unloadedRulesText.setText(rule);
+                    generateRuleToggle = false;
+                } else {
+                    injectedRulesText.setText(rule);
+                }
             }
         });
     }
@@ -573,6 +727,55 @@
         });
     }
 
+    // Package private for testing
+    String getInjectedRuleContent() throws InvocationTargetException, InterruptedException {
+        injectedRuleContent = "";
+        SwingUtilities.invokeAndWait(new Runnable() {
+            @Override
+            public void run() {
+                injectedRuleContent = injectedRulesText.getText();
+            }
+        });
+        return injectedRuleContent;
+    }
+
+    // Package private for testing
+    void setUnloadedRuleContent(final String rule) throws InvocationTargetException, InterruptedException {
+        SwingUtilities.invokeAndWait(new Runnable() {
+            @Override
+            public void run() {
+                unloadedRulesText.setText(rule);
+            }
+        });
+    }
+
+    // Package private for testing
+    String getUnloadedRuleContent() throws InvocationTargetException, InterruptedException {
+        unloadedRuleContent = "";
+        SwingUtilities.invokeAndWait(new Runnable() {
+            @Override
+            public void run() {
+                unloadedRuleContent = unloadedRulesText.getText();
+            }
+        });
+        return unloadedRuleContent;
+    }
+
+    // Package private for testing
+    boolean isInjectButtonEnabled() {
+        return injectRuleButton.isEnabled();
+    }
+
+    // Package private for testing
+    boolean isUnloadButtonEnabled() {
+        return unloadRuleButton.isEnabled();
+    }
+
+    // Package private for testing
+    void enableGenerateRuleToggle() {
+        generateRuleToggle = true;
+    }
+
     private void updateGraphInView(List<BytemanMetric> metrics, String xkey, String ykey, String filter, String value, String graphtype) {
         final List<BytemanMetric> ms = metrics;
         final String xk = xkey;
@@ -710,7 +913,7 @@
 
                 @Override
                 public void run() {
-                    content[0] = rulesText.getText();
+                    content[0] = unloadedRulesText.getText();
                 }
                 
             });
--- a/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/VmBytemanInformationController.java	Fri Aug 26 11:48:51 2016 -0400
+++ b/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/VmBytemanInformationController.java	Fri Aug 26 14:08:00 2016 -0400
@@ -84,6 +84,7 @@
     private static final Logger logger = LoggingUtils.getLogger(VmBytemanInformationController.class);
     private static final Translate<LocaleResources> t = LocaleResources.createLocalizer();
     private static final Charset UTF_8_CHARSET = Charset.forName("UTF-8");
+    private static final String EMPTY_STR = "";
     static final String NO_RULES_LOADED = t.localize(LocaleResources.NO_RULES_LOADED).getContents();
     
     private final VmRef vm;
@@ -365,7 +366,7 @@
     }
 
     private boolean isEmpty(String rule) {
-        if (rule == null) {
+        if (EMPTY_STR.equals(rule.trim())) {
             return true;
         }
         return NO_RULES_LOADED.equals(rule);
--- a/vm-byteman/client-swing/src/main/resources/com/redhat/thermostat/vm/byteman/client/swing/internal/strings.properties	Fri Aug 26 11:48:51 2016 -0400
+++ b/vm-byteman/client-swing/src/main/resources/com/redhat/thermostat/vm/byteman/client/swing/internal/strings.properties	Fri Aug 26 14:08:00 2016 -0400
@@ -10,3 +10,6 @@
 RULE_EMPTY = Rule to inject is empty.
 NO_RULES_LOADED = <no-rules-loaded>
 NO_METRICS_AVAILABLE = <no-metrics-available>
+LABEL_LOCAL_RULE = Local Rule
+LABEL_INJECTED_RULE = Injected Rule
+IMPORT_RULE = Import Rule from File
--- a/vm-byteman/client-swing/src/test/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanViewTest.java	Fri Aug 26 11:48:51 2016 -0400
+++ b/vm-byteman/client-swing/src/test/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanViewTest.java	Fri Aug 26 14:08:00 2016 -0400
@@ -37,6 +37,9 @@
 package com.redhat.thermostat.vm.byteman.client.swing.internal;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import java.awt.Container;
 import java.awt.Dimension;
@@ -45,13 +48,26 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
+import java.util.concurrent.CountDownLatch;
 
 import javax.swing.JFrame;
 import javax.swing.JTextArea;
 import javax.swing.SwingUtilities;
 import javax.swing.text.JTextComponent;
 
+import com.redhat.thermostat.client.command.RequestQueue;
+import com.redhat.thermostat.common.ActionListener;
 import com.redhat.thermostat.common.Clock;
+import com.redhat.thermostat.storage.core.AgentId;
+import com.redhat.thermostat.storage.core.HostRef;
+import com.redhat.thermostat.storage.core.VmId;
+import com.redhat.thermostat.storage.core.VmRef;
+import com.redhat.thermostat.storage.dao.AgentInfoDAO;
+import com.redhat.thermostat.storage.dao.VmInfoDAO;
+import com.redhat.thermostat.storage.model.AgentInformation;
+import com.redhat.thermostat.storage.model.VmInfo;
+import com.redhat.thermostat.vm.byteman.client.swing.internal.VmBytemanView.GenerateAction;
+import com.redhat.thermostat.vm.byteman.common.VmBytemanDAO;
 import org.fest.swing.annotation.GUITest;
 import org.fest.swing.core.NameMatcher;
 import org.fest.swing.edt.FailOnThreadViolationRepaintManager;
@@ -109,7 +125,66 @@
         frame = null;
         view = null;
     }
-    
+
+    @GUITest
+    @Test
+    public void testShifterInjectAndUnloadButtons() throws InvocationTargetException, InterruptedException {
+        String content = "RULE foo bar baz";
+        String newContent = "RULE new rule";
+        String mainClass = "foo-main-class";
+
+        // Initial set-up
+        assertEquals(true, view.isInjectButtonEnabled());
+        assertEquals(false, view.isUnloadButtonEnabled());
+        assertEquals(t.localize(LocaleResources.NO_RULES_LOADED).getContents(), view.getInjectedRuleContent());
+        assertEquals(t.localize(LocaleResources.NO_RULES_LOADED).getContents(), view.getUnloadedRuleContent());
+
+        testInjectRule(content);
+        testPreservingRuleDuringActionEvent(content, newContent);
+        testUnloadRule(newContent);
+        testGenerateRule(mainClass);
+    }
+
+    private void testInjectRule(String content) throws InvocationTargetException, InterruptedException {
+        view.setUnloadedRuleContent(content);
+        assertEquals(content, view.getUnloadedRuleContent());
+        setInjectStateInEDT(VmBytemanView.BytemanInjectState.INJECTED);
+        assertEquals(content, view.getInjectedRuleContent());
+        assertEquals(false, view.isInjectButtonEnabled());
+        assertEquals(true, view.isUnloadButtonEnabled());
+    }
+
+    private void testPreservingRuleDuringActionEvent(String oldContent, String newContent) throws InvocationTargetException, InterruptedException {
+        view.setUnloadedRuleContent(newContent);
+        ActionEvent<VmBytemanView.TabbedPaneContentAction> event = new ActionEvent<>(this, VmBytemanView.TabbedPaneContentAction.RULES_CHANGED);
+        event.setPayload(oldContent);
+        view.contentChanged(event);
+        assertEquals(newContent, view.getUnloadedRuleContent());
+        assertEquals(oldContent, view.getInjectedRuleContent());
+    }
+
+    private void testUnloadRule(String content) throws InvocationTargetException, InterruptedException {
+        setInjectStateInEDT(VmBytemanView.BytemanInjectState.UNLOADED);
+        ActionEvent event = new ActionEvent<>(this, VmBytemanView.TabbedPaneContentAction.RULES_CHANGED);
+        event.setPayload(t.localize(LocaleResources.NO_RULES_LOADED).getContents());
+        view.contentChanged(event);
+        assertEquals(t.localize(LocaleResources.NO_RULES_LOADED).getContents(), view.getInjectedRuleContent());
+        assertEquals(content, view.getUnloadedRuleContent());
+        assertEquals(true, view.isInjectButtonEnabled());
+        assertEquals(false, view.isUnloadButtonEnabled());
+    }
+
+    private void testGenerateRule(String mainClass) throws InvocationTargetException, InterruptedException {
+        VmBytemanInformationController controller = createController();
+        String template = controller.generateTemplateForVM(mainClass);
+        ActionEvent event = new ActionEvent<>(this, VmBytemanView.TabbedPaneContentAction.RULES_CHANGED);
+        event.setPayload(template);
+        view.enableGenerateRuleToggle();
+        view.contentChanged(event);
+        assertEquals(template, view.getUnloadedRuleContent());
+    }
+
+
     @GUITest
     @Test
     public void testInjectButton() throws InvocationTargetException, InterruptedException {
@@ -140,13 +215,19 @@
         });
         checkButtonState(VmBytemanView.BytemanInjectState.UNLOADING, toggleButton);
         assertEquals(t.localize(LocaleResources.UNLOAD_RULE).getContents(), toggleButtonFixture.text());
+        assertEquals(view.isInjectButtonEnabled(), false);
+        assertEquals(view.isUnloadButtonEnabled(), true);
 
         setInjectStateInEDT(VmBytemanView.BytemanInjectState.INJECTED);
         checkButtonState(VmBytemanView.BytemanInjectState.INJECTED, toggleButton);
+        assertEquals(view.isInjectButtonEnabled(), false);
+        assertEquals(view.isUnloadButtonEnabled(), true);
         assertEquals(t.localize(LocaleResources.UNLOAD_RULE).getContents(), toggleButtonFixture.text());
 
         setInjectStateInEDT(VmBytemanView.BytemanInjectState.UNLOADED);
         checkButtonState(VmBytemanView.BytemanInjectState.UNLOADED, toggleButton);
+        assertEquals(view.isInjectButtonEnabled(), true);
+        assertEquals(view.isUnloadButtonEnabled(), false);
         assertEquals(t.localize(LocaleResources.INJECT_RULE).getContents(), toggleButtonFixture.text());
 
         setInjectStateInEDT(VmBytemanView.BytemanInjectState.DISABLED);
@@ -156,29 +237,31 @@
         setInjectStateInEDT(VmBytemanView.BytemanInjectState.INJECTING);
         checkButtonState(VmBytemanView.BytemanInjectState.INJECTING, toggleButton);
         assertEquals(t.localize(LocaleResources.INJECT_RULE).getContents(), toggleButtonFixture.text());
+        assertEquals(view.isInjectButtonEnabled(), true);
+        assertEquals(view.isUnloadButtonEnabled(), false);
     }
-    
+
     @GUITest
     @Test
-    public void testGetRuleContent() {
+    public void testGetRuleContent() throws InvocationTargetException, InterruptedException {
         String ruleContent = "";
         setRuleContentInEDT(ruleContent);
-        
-        String actual = view.getRuleContent();
+
+        String actual = view.getUnloadedRuleContent();
         assertEquals(ruleContent, actual);
-        
+
         ruleContent = "foo\nbar\nbaz";
         setRuleContentInEDT(ruleContent);
-        actual = view.getRuleContent();
+        actual = view.getUnloadedRuleContent();
         assertEquals(ruleContent, actual);
     }
-    
+
     @GUITest
     @Test
     public void testContentChangedMetrics() {
         String content = "{ \"foo\": \"bar\" }";
         String marker = "marker";
-        long timestamp  = 1_440_000_000_000L;
+        long timestamp = 1_440_000_000_000L;
         DateFormat metricsDateFormat = SwingVmBytemanView.metricsDateFormat;
         String timestring = metricsDateFormat.format(new Date(timestamp));
         BytemanMetric m = new BytemanMetric();
@@ -186,39 +269,44 @@
         m.setMarker(marker);
         m.setTimeStamp(timestamp);
 
-        
         ActionEvent<VmBytemanView.TabbedPaneContentAction> event = new ActionEvent<>(this, VmBytemanView.TabbedPaneContentAction.METRICS_CHANGED);
         event.setPayload(Arrays.asList(m));
         view.contentChanged(event);
         verifyMetricsTextEquals(timestring + ": " + marker + " " + content + "\n");
-        
+
         // Do the same with an empty metrics list
         event = new ActionEvent<>(this, VmBytemanView.TabbedPaneContentAction.METRICS_CHANGED);
         event.setPayload(Collections.emptyList());
         view.contentChanged(event);
         verifyMetricsTextEquals(SwingVmBytemanView.NO_METRICS_AVAILABLE + "\n");
     }
-    
+
     @GUITest
     @Test
-    public void testContentChangedRules() {
-        String content = "RULE foo bar baz"; 
-        
+    public void testContentChangedRules() throws InvocationTargetException, InterruptedException {
+
+        String content = "RULE foo bar baz";
+
+        // Unloaded rules shouldn't be overwritten if they already have content in them
+        String unloadedRule = "RULE baz bar foo";
+        view.setUnloadedRuleContent(unloadedRule);
+        assertEquals(view.getUnloadedRuleContent(), unloadedRule);
         ActionEvent<VmBytemanView.TabbedPaneContentAction> event = new ActionEvent<>(this, VmBytemanView.TabbedPaneContentAction.RULES_CHANGED);
         event.setPayload(content);
         view.contentChanged(event);
-        verifyRulesTextEquals(content);
-        
+        verifyUnloadedRulesTextEquals(unloadedRule);
+
         // Do the same, now setting empty
         content = "";
+        view.setUnloadedRuleContent(content);
         event = new ActionEvent<>(this, VmBytemanView.TabbedPaneContentAction.RULES_CHANGED);
         event.setPayload(content);
         view.contentChanged(event);
-        verifyRulesTextEquals(content);
+        verifyUnloadedRulesTextEquals(content);
     }
 
-    private void verifyRulesTextEquals(String expected) {
-        final JTextComponent text = getJTextAreaWithName(SwingVmBytemanView.RULES_TEXT_NAME);
+    private void verifyUnloadedRulesTextEquals(String expected) {
+        final JTextComponent text = getJTextAreaWithName(SwingVmBytemanView.RULES_UNLOADED_TEXT_NAME);
         verifyEquals(text, expected);
     }
 
@@ -226,7 +314,7 @@
         final JTextComponent text = getJTextAreaWithName(SwingVmBytemanView.METRICS_TEXT_NAME);
         verifyEquals(text, expected);
     }
-    
+
     private void verifyEquals(final JTextComponent text, final String expected) {
         GuiActionRunner.execute(new GuiTask() {
             @Override
@@ -237,7 +325,7 @@
     }
 
     private void setRuleContentInEDT(final String ruleContent) {
-        final JTextComponent text = getJTextAreaWithName(SwingVmBytemanView.RULES_TEXT_NAME);
+        final JTextComponent text = getJTextAreaWithName(SwingVmBytemanView.RULES_UNLOADED_TEXT_NAME);
         GuiActionRunner.execute(new GuiTask() {
             @Override
             protected void executeInEDT() throws Throwable {
@@ -264,9 +352,9 @@
                 assertEquals(reference.isSelected(), toggleButton.isSelected());
             }
         });
-        
+
     }
-    
+
     private void setInjectStateInEDT(final VmBytemanView.BytemanInjectState state) {
         GuiActionRunner.execute(new GuiTask() {
             @Override
@@ -289,7 +377,7 @@
         frame.pack();
         frame.setVisible(true);
     }
-    
+
     // For basic, quick and dirty UI testing
     public static void main(String[] args) {
         javax.swing.SwingUtilities.invokeLater(new Runnable() {
@@ -299,4 +387,28 @@
         });
     }
 
+    private VmBytemanInformationController createController() {
+        VmRef ref = mock(VmRef.class);
+        when(ref.getVmId()).thenReturn("some-vm-id");
+        when(ref.getHostRef()).thenReturn(new HostRef("some-agent-id", "some-host-name"));
+        AgentInfoDAO agentInfoDao = mock(AgentInfoDAO.class);
+        AgentInformation agentInfo = mock(AgentInformation.class);
+        when(agentInfo.isAlive()).thenReturn(true);
+        when(agentInfoDao.getAgentInformation(any(AgentId.class))).thenReturn(agentInfo);
+        VmInfoDAO vmInfoDao = mock(VmInfoDAO.class);
+        VmInfo vmInfo = mock(VmInfo.class);
+        when(vmInfo.isAlive(agentInfo)).thenReturn(VmInfo.AliveStatus.RUNNING);
+        when(vmInfoDao.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
+        when(vmInfoDao.getVmInfo(any(VmRef.class))).thenReturn(vmInfo);
+        VmBytemanDAO vmBytemanDao = mock(VmBytemanDAO.class);
+        RequestQueue requestQueue = mock(RequestQueue.class);
+        return new VmBytemanInformationController(view, ref, agentInfoDao, vmInfoDao, vmBytemanDao, requestQueue) {
+
+            @Override
+            void waitWithTimeOut(CountDownLatch latch) {
+                // nothing, return immediately for tests
+            }
+        };
+    }
+
 }