changeset 54:c2fa392782bd

update the agent/vm tree asynchronously
author Omair Majid <omajid@redhat.com>
date Wed, 25 Jan 2012 12:18:00 -0500
parents fceb773a3938
children fc0ff5ca8e76
files src/com/redhat/thermostat/client/DummyFacade.java src/com/redhat/thermostat/client/HostPanelFacade.java src/com/redhat/thermostat/client/HostRef.java src/com/redhat/thermostat/client/MainWindowFacade.java src/com/redhat/thermostat/client/MainWindowFacadeImpl.java src/com/redhat/thermostat/client/Ref.java src/com/redhat/thermostat/client/SummaryPanelFacade.java src/com/redhat/thermostat/client/VmRef.java src/com/redhat/thermostat/client/ui/MainWindow.java src/com/redhat/thermostat/common/utils/StringUtils.java
diffstat 10 files changed, 347 insertions(+), 97 deletions(-) [+]
line wrap: on
line diff
--- a/src/com/redhat/thermostat/client/DummyFacade.java	Wed Jan 25 11:49:32 2012 -0500
+++ b/src/com/redhat/thermostat/client/DummyFacade.java	Wed Jan 25 12:18:00 2012 -0500
@@ -45,6 +45,10 @@
 import java.util.Map;
 import java.util.Random;
 
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.TreeModel;
+
 import com.redhat.thermostat.common.HostInfo;
 import com.redhat.thermostat.common.NetworkInfo;
 import com.redhat.thermostat.common.NetworkInterfaceInfo;
@@ -60,6 +64,7 @@
 
     private HostRef onlyAgent = new HostRef("a-random-string-of-letters-and-numbers", "agent on localhost");
     private VmRef onlyVm = new VmRef(onlyAgent, "a-random-string-of-letters-and-numbers-or-perhaps-a-process-id", "super crazy awesome java app");
+    private String filter;
 
     public DummyFacade() {
         toDisplay.addAll(Arrays.asList(MemoryType.values()));
@@ -86,6 +91,16 @@
     }
 
     @Override
+    public TreeModel getHostVmTree() {
+        return new DefaultTreeModel(new DefaultMutableTreeNode());
+    }
+
+    @Override
+    public void setHostVmTreeFilter(String filter) {
+        this.filter = filter;
+    }
+
+    @Override
     public List<String> getIssues() {
         return new ArrayList<String>();
     }
@@ -293,4 +308,14 @@
         return stat;
     }
 
+    @Override
+    public void start() {
+        // no-op
+    }
+
+    @Override
+    public void stop() {
+        // no-op
+    }
+
 }
--- a/src/com/redhat/thermostat/client/HostPanelFacade.java	Wed Jan 25 11:49:32 2012 -0500
+++ b/src/com/redhat/thermostat/client/HostPanelFacade.java	Wed Jan 25 12:18:00 2012 -0500
@@ -40,18 +40,18 @@
 import com.redhat.thermostat.common.NetworkInfo;
 
 public interface HostPanelFacade {
-    public abstract HostInfo getHostInfo();
+    public HostInfo getHostInfo();
 
-    public abstract NetworkInfo getNetworkInfo();
+    public NetworkInfo getNetworkInfo();
 
-    public abstract DiscreteTimeData<Double>[] getCpuLoad();
+    public DiscreteTimeData<Double>[] getCpuLoad();
 
-    public abstract DiscreteTimeData<Long>[] getMemoryUsage(MemoryType type);
+    public DiscreteTimeData<Long>[] getMemoryUsage(MemoryType type);
 
-    public abstract MemoryType[] getMemoryTypesToDisplay();
+    public MemoryType[] getMemoryTypesToDisplay();
 
-    public abstract boolean isMemoryTypeDisplayed(MemoryType type);
+    public boolean isMemoryTypeDisplayed(MemoryType type);
 
-    public abstract void setDisplayMemoryType(MemoryType type, boolean selected);
+    public void setDisplayMemoryType(MemoryType type, boolean selected);
 
 }
--- a/src/com/redhat/thermostat/client/HostRef.java	Wed Jan 25 11:49:32 2012 -0500
+++ b/src/com/redhat/thermostat/client/HostRef.java	Wed Jan 25 12:18:00 2012 -0500
@@ -36,7 +36,7 @@
 
 package com.redhat.thermostat.client;
 
-public class HostRef {
+public class HostRef implements Ref {
 
     private final String uid;
     private final String name;
@@ -59,6 +59,34 @@
         return name;
     }
 
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (obj.getClass() != this.getClass()) {
+            return false;
+        }
+        HostRef other = (HostRef) obj;
+        if (equals(this.uid, other.uid) && equals(this.name, other.name)) {
+            return true;
+        }
+        return false;
+    }
+
+    private static boolean equals(Object obj1, Object obj2) {
+        return (obj1 == null && obj2 == null) || (obj1 != null && obj1.equals(obj2));
+    }
+
+    @Override
+    public int hashCode() {
+        return uid.hashCode();
+    }
+
+    @Override
     public boolean matches(String filter) {
         return getHostName().contains(filter) || getAgentId().contains(filter);
     }
--- a/src/com/redhat/thermostat/client/MainWindowFacade.java	Wed Jan 25 11:49:32 2012 -0500
+++ b/src/com/redhat/thermostat/client/MainWindowFacade.java	Wed Jan 25 12:18:00 2012 -0500
@@ -36,10 +36,20 @@
 
 package com.redhat.thermostat.client;
 
+import javax.swing.tree.TreeModel;
+
 public interface MainWindowFacade {
 
-    public abstract HostRef[] getHosts();
+    public void start();
+
+    public void stop();
+
+    public HostRef[] getHosts();
 
-    public abstract VmRef[] getVms(HostRef ref);
+    public VmRef[] getVms(HostRef ref);
+
+    public TreeModel getHostVmTree();
+
+    public void setHostVmTreeFilter(String filter);
 
 }
--- a/src/com/redhat/thermostat/client/MainWindowFacadeImpl.java	Wed Jan 25 11:49:32 2012 -0500
+++ b/src/com/redhat/thermostat/client/MainWindowFacadeImpl.java	Wed Jan 25 12:18:00 2012 -0500
@@ -36,32 +36,73 @@
 
 package com.redhat.thermostat.client;
 
+import static com.redhat.thermostat.client.Translate._;
+
+import java.io.PrintStream;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import javax.swing.SwingWorker;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.TreeModel;
+import javax.swing.tree.TreeNode;
+
 import com.mongodb.BasicDBObject;
 import com.mongodb.DB;
 import com.mongodb.DBCollection;
 import com.mongodb.DBCursor;
 import com.mongodb.DBObject;
 import com.redhat.thermostat.common.utils.LoggingUtils;
+import com.redhat.thermostat.common.utils.StringUtils;
 
 public class MainWindowFacadeImpl implements MainWindowFacade {
 
     private static final Logger logger = LoggingUtils.getLogger(MainWindowFacadeImpl.class);
 
+    private final DefaultMutableTreeNode publishedRoot = new DefaultMutableTreeNode(_("MAIN_WINDOW_TREE_ROOT_NAME"));
+    private final DefaultTreeModel publishedTreeModel = new DefaultTreeModel(publishedRoot);
+
     private DB db;
     private DBCollection agentConfigCollection;
     private DBCollection hostInfoCollection;
     private DBCollection vmInfoCollection;
 
+    private String filterText;
+
+    private Timer backgroundUpdater;
+
     public MainWindowFacadeImpl(DB db) {
         this.db = db;
         this.agentConfigCollection = db.getCollection("agent-config");
         this.hostInfoCollection = db.getCollection("host-info");
         this.vmInfoCollection = db.getCollection("vm-info");
+
+    }
+
+    @Override
+    public void start() {
+        backgroundUpdater = new Timer();
+        backgroundUpdater.scheduleAtFixedRate(new TimerTask() {
+            @Override
+            public void run() {
+                doUpdateTreeAsync();
+            }
+        }, 0, TimeUnit.SECONDS.toMillis(10));
+
+    }
+
+    @Override
+    public void stop() {
+        backgroundUpdater.cancel();
     }
 
     @Override
@@ -99,5 +140,137 @@
         return vmRefs.toArray(new VmRef[0]);
     }
 
+    @Override
+    public TreeModel getHostVmTree() {
+        return publishedTreeModel;
+    }
+
+    private Ref[] getChildren(Ref parent) {
+        if (parent == null) {
+            return getHosts();
+        } else if (parent instanceof HostRef) {
+            HostRef host = (HostRef) parent;
+            return getVms(host);
+        }
+        return new Ref[0];
+    }
+
+    @Override
+    public void setHostVmTreeFilter(String filter) {
+        this.filterText = filter;
+        doUpdateTreeAsync();
+    }
+
+    public void doUpdateTreeAsync() {
+        BackgroundTreeModelWorker worker = new BackgroundTreeModelWorker(this, publishedTreeModel, publishedRoot);
+        worker.execute();
+    }
+
+    /**
+     * Updates a TreeModel in the background in an Swing EDT-safe manner.
+     */
+    private static class BackgroundTreeModelWorker extends SwingWorker<DefaultMutableTreeNode, Void> {
+
+        private final DefaultTreeModel treeModel;
+        private MainWindowFacadeImpl facade;
+        private DefaultMutableTreeNode treeRoot;
+
+        public BackgroundTreeModelWorker(MainWindowFacadeImpl facade, DefaultTreeModel model, DefaultMutableTreeNode root) {
+            this.facade = facade;
+            this.treeModel = model;
+            this.treeRoot = root;
+        }
+
+        @Override
+        protected DefaultMutableTreeNode doInBackground() throws Exception {
+            DefaultMutableTreeNode root = new DefaultMutableTreeNode();
+            List<HostRef> hostsInRemoteModel = Arrays.asList(facade.getHosts());
+            buildSubTree(root, hostsInRemoteModel, facade.filterText);
+            return root;
+        }
+
+        private boolean buildSubTree(DefaultMutableTreeNode parent, List<? extends Ref> objectsInRemoteModel, String filter) {
+            boolean subTreeMatches = false;
+            for (Ref inRemoteModel : objectsInRemoteModel) {
+                DefaultMutableTreeNode inTreeNode = new DefaultMutableTreeNode(inRemoteModel);
+
+                boolean shouldInsert = false;
+                if (filter == null || inRemoteModel.matches(filter)) {
+                    shouldInsert = true;
+                }
+
+                List<Ref> children = Arrays.asList(facade.getChildren(inRemoteModel));
+                boolean subtreeResult = buildSubTree(inTreeNode, children, filter);
+                if (subtreeResult) {
+                    shouldInsert = true;
+                }
+
+                if (shouldInsert) {
+                    parent.add(inTreeNode);
+                    subTreeMatches = true;
+                }
+            }
+            return subTreeMatches;
+        }
+
+        @Override
+        protected void done() {
+            DefaultMutableTreeNode sourceRoot;
+            try {
+                sourceRoot = get();
+                syncTree(sourceRoot, treeModel, treeRoot);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            } catch (ExecutionException e) {
+                e.printStackTrace();
+            }
+        }
+
+        private void syncTree(DefaultMutableTreeNode sourceRoot, DefaultTreeModel targetModel, DefaultMutableTreeNode targetNode) {
+            List<DefaultMutableTreeNode> sourceChildren = Collections.list(sourceRoot.children());
+            List<DefaultMutableTreeNode> targetChildren = Collections.list(targetNode.children());
+            for (DefaultMutableTreeNode sourceChild : sourceChildren) {
+                Ref sourceRef = (Ref) sourceChild.getUserObject();
+                DefaultMutableTreeNode targetChild = null;
+                for (DefaultMutableTreeNode aChild : targetChildren) {
+                    Ref targetRef = (Ref) aChild.getUserObject();
+                    if (targetRef.equals(sourceRef)) {
+                        targetChild = aChild;
+                        break;
+                    }
+                }
+
+                if (targetChild == null) {
+                    targetChild = new DefaultMutableTreeNode(sourceRef);
+                    targetModel.insertNodeInto(targetChild, targetNode, targetNode.getChildCount());
+                }
+
+                syncTree(sourceChild, targetModel, targetChild);
+            }
+
+            for (DefaultMutableTreeNode targetChild : targetChildren) {
+                Ref targetRef = (Ref) targetChild.getUserObject();
+                boolean matchFound = false;
+                for (DefaultMutableTreeNode sourceChild : sourceChildren) {
+                    Ref sourceRef = (Ref) sourceChild.getUserObject();
+                    if (targetRef.equals(sourceRef)) {
+                        matchFound = true;
+                        break;
+                    }
+                }
+
+                if (!matchFound) {
+                    targetModel.removeNodeFromParent(targetChild);
+                }
+            }
+        }
+    }
+
+    private static void printTree(PrintStream out, TreeNode node, int depth) {
+        out.println(StringUtils.repeat("  ", depth) + node.toString());
+        for (TreeNode child : (List<TreeNode>) Collections.list(node.children())) {
+            printTree(out, child, depth + 1);
+        }
+    }
 
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/com/redhat/thermostat/client/Ref.java	Wed Jan 25 12:18:00 2012 -0500
@@ -0,0 +1,43 @@
+/*
+ * 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;
+
+public interface Ref {
+
+    public boolean matches(String filter);
+
+}
--- a/src/com/redhat/thermostat/client/SummaryPanelFacade.java	Wed Jan 25 11:49:32 2012 -0500
+++ b/src/com/redhat/thermostat/client/SummaryPanelFacade.java	Wed Jan 25 12:18:00 2012 -0500
@@ -40,9 +40,9 @@
 
 public interface SummaryPanelFacade {
 
-    public abstract long getTotalConnectedVms();
+    public long getTotalConnectedVms();
 
-    public abstract long getTotalConnectedAgents();
+    public long getTotalConnectedAgents();
 
     public List<String> getIssues();
 
--- a/src/com/redhat/thermostat/client/VmRef.java	Wed Jan 25 11:49:32 2012 -0500
+++ b/src/com/redhat/thermostat/client/VmRef.java	Wed Jan 25 12:18:00 2012 -0500
@@ -36,7 +36,7 @@
 
 package com.redhat.thermostat.client;
 
-public class VmRef {
+public class VmRef implements Ref {
 
     private final HostRef hostRef;
     private final String uid;
@@ -65,6 +65,34 @@
         return name;
     }
 
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (obj.getClass() != this.getClass()) {
+            return false;
+        }
+        VmRef other = (VmRef) obj;
+        if (equals(this.hostRef, other.hostRef) && equals(this.uid, other.uid) && equals(this.name, other.name)) {
+            return true;
+        }
+        return false;
+    }
+
+    private static boolean equals(Object obj1, Object obj2) {
+        return (obj1 == null && obj2 == null) || (obj1 != null && obj1.equals(obj2));
+    }
+
+    @Override
+    public int hashCode() {
+        return uid.hashCode();
+    }
+
+    @Override
     public boolean matches(String filter) {
         return getName().contains(filter) || getId().contains(filter);
     }
--- a/src/com/redhat/thermostat/client/ui/MainWindow.java	Wed Jan 25 11:49:32 2012 -0500
+++ b/src/com/redhat/thermostat/client/ui/MainWindow.java	Wed Jan 25 12:18:00 2012 -0500
@@ -47,8 +47,6 @@
 import java.awt.event.ActionListener;
 import java.awt.event.InputEvent;
 import java.awt.event.KeyEvent;
-import java.util.ArrayList;
-import java.util.List;
 
 import javax.swing.BorderFactory;
 import javax.swing.JFrame;
@@ -63,7 +61,6 @@
 import javax.swing.JTextField;
 import javax.swing.JTree;
 import javax.swing.KeyStroke;
-import javax.swing.ScrollPaneConstants;
 import javax.swing.ToolTipManager;
 import javax.swing.event.DocumentEvent;
 import javax.swing.event.DocumentListener;
@@ -73,14 +70,11 @@
 import javax.swing.text.Document;
 import javax.swing.tree.DefaultMutableTreeNode;
 import javax.swing.tree.DefaultTreeCellRenderer;
-import javax.swing.tree.DefaultTreeModel;
 import javax.swing.tree.TreeModel;
-import javax.swing.tree.TreeNode;
 import javax.swing.tree.TreePath;
 import javax.swing.tree.TreeSelectionModel;
 
 import com.redhat.thermostat.client.ApplicationInfo;
-import com.redhat.thermostat.client.ClientArgs;
 import com.redhat.thermostat.client.HostRef;
 import com.redhat.thermostat.client.MainWindowFacade;
 import com.redhat.thermostat.client.UiFacadeFactory;
@@ -90,15 +84,13 @@
 
     private static final long serialVersionUID = 5608972421496808177L;
 
-    private final DefaultMutableTreeNode root = new DefaultMutableTreeNode(_("MAIN_WINDOW_TREE_ROOT_NAME"));
-    private final DefaultTreeModel treeModel = new DefaultTreeModel(root);
-
     private final UiFacadeFactory facadeFactory;
     private final MainWindowFacade facade;
 
     private JPanel contentArea = null;
+
+    private JTextField searchField = null;
     private JTree agentVmTree = null;
-    private JTextField searchField = null;
 
     public MainWindow(UiFacadeFactory facadeFactory) {
         super();
@@ -106,9 +98,10 @@
 
         this.facadeFactory = facadeFactory;
         this.facade = facadeFactory.getMainWindow();
-
         searchField = new JTextField();
-        agentVmTree = new AgentVmTree(treeModel);
+        TreeModel model = facade.getHostVmTree();
+        agentVmTree = new JTree(model);
+        agentVmTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
         agentVmTree.setCellRenderer(new AgentVmTreeCellRenderer());
         ToolTipManager.sharedInstance().registerComponent(agentVmTree);
         contentArea = new JPanel(new BorderLayout());
@@ -116,11 +109,11 @@
         setupMenus();
         setupPanels();
 
-        agentVmTree.setSelectionPath(new TreePath(root.getPath()));
-
-        buildTree("");
+        agentVmTree.setSelectionPath(new TreePath(((DefaultMutableTreeNode) model.getRoot()).getPath()));
 
         setDefaultCloseOperation(DISPOSE_ON_CLOSE);
+
+        this.facade.start();
     }
 
     private void setupMenus() {
@@ -151,7 +144,7 @@
 
         JMenuItem fileExitMenu = new JMenuItem(_("MENU_FILE_EXIT"));
         fileExitMenu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, InputEvent.CTRL_DOWN_MASK));
-        fileExitMenu.addActionListener(new ShtudownClient(this));
+        fileExitMenu.addActionListener(new ShutdownClient(this.facade, this));
         fileMenu.add(fileExitMenu);
 
         JMenu helpMenu = new JMenu(_("MENU_HELP"));
@@ -203,10 +196,13 @@
                 String filter = null;
                 try {
                     filter = doc.getText(0, doc.getLength());
+                    if (filter.trim().equals("")) {
+                        filter = null;
+                    }
                 } catch (BadLocationException ble) {
                     // ignore
                 }
-                buildTree(filter);
+                facade.setHostVmTreeFilter(filter);
             }
         });
 
@@ -236,7 +232,6 @@
                     contentArea.revalidate();
                 }
             }
-
         });
 
         JScrollPane treeScrollPane = new JScrollPane(agentVmTree);
@@ -257,81 +252,20 @@
         return result;
     }
 
-    private void buildTree(String filter) {
-        root.removeAllChildren();
-        treeModel.setRoot(null);
-        // paths to expand. only expand paths when a vm matches (to ensure it is
-        // visible)
-
-        List<TreeNode[]> pathsToExpand = new ArrayList<TreeNode[]>();
-        if (filter == null || filter.trim().equals("")) {
-            DefaultMutableTreeNode agentNode;
-            HostRef[] agentRefs = facade.getHosts();
-            for (HostRef hostRef : agentRefs) {
-                agentNode = new DefaultMutableTreeNode(hostRef);
-                root.add(agentNode);
-                VmRef[] vmRefs = facade.getVms(hostRef);
-                for (VmRef vmRef : vmRefs) {
-                    agentNode.add(new DefaultMutableTreeNode(vmRef));
-                }
-            }
-            treeModel.setRoot(root);
-        } else {
-            DefaultMutableTreeNode agentNode;
-            for (HostRef hostRef : facade.getHosts()) {
-                if (hostRef.matches(filter)) {
-                    agentNode = new DefaultMutableTreeNode(hostRef);
-                    root.add(agentNode);
-                    VmRef[] vmRefs = facade.getVms(hostRef);
-                    for (VmRef vmRef : vmRefs) {
-                        agentNode.add(new DefaultMutableTreeNode(vmRef));
-                    }
-                } else {
-                    agentNode = null;
-                    for (VmRef vmRef : facade.getVms(hostRef)) {
-                        if (vmRef.matches(filter)) {
-                            if (agentNode == null) {
-                                agentNode = new DefaultMutableTreeNode(hostRef);
-                                root.add(agentNode);
-                            }
-                            DefaultMutableTreeNode vmNode = new DefaultMutableTreeNode(vmRef);
-                            agentNode.add(vmNode);
-                            pathsToExpand.add(vmNode.getPath());
-                        }
-                    }
-                }
-            }
-            if (root.getChildCount() > 0) {
-                treeModel.setRoot(root);
-            }
-
-        }
-        for (TreeNode[] path : pathsToExpand) {
-            agentVmTree.expandPath(new TreePath(path).getParentPath());
-        }
-        agentVmTree.expandRow(0);
-    }
-
-    public static class ShtudownClient implements ActionListener {
+    public static class ShutdownClient implements ActionListener {
 
         private JFrame toDispose;
+        private MainWindowFacade facade;
 
-        public ShtudownClient(JFrame toDispose) {
+        public ShutdownClient(MainWindowFacade facade, JFrame toDispose) {
+            this.facade = facade;
             this.toDispose = toDispose;
         }
 
         @Override
         public void actionPerformed(ActionEvent e) {
             toDispose.dispose();
-        }
-    }
-
-    private static class AgentVmTree extends JTree {
-        private static final long serialVersionUID = 8894141735861100579L;
-
-        public AgentVmTree(TreeModel model) {
-            super(model);
-            getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
+            facade.stop();
         }
     }
 
--- a/src/com/redhat/thermostat/common/utils/StringUtils.java	Wed Jan 25 11:49:32 2012 -0500
+++ b/src/com/redhat/thermostat/common/utils/StringUtils.java	Wed Jan 25 12:18:00 2012 -0500
@@ -57,4 +57,13 @@
     public static String quote(String toQuote) {
         return new String("\"" + toQuote + "\"");
     }
+
+    public static String repeat(String text, int times) {
+        StringBuilder builder = new StringBuilder(text.length() * times);
+        for (int i = 0; i < times; i++) {
+            builder.append(text);
+        }
+        return builder.toString();
+    }
+
 }