changeset 995:db2b89cdef62

Merge
author Roman Kennke <rkennke@redhat.com>
date Fri, 22 Feb 2013 16:53:10 +0100
parents e318ef62411d (current diff) e7ffb5dcd2dd (diff)
children a5e29a0c324e 0d9534310a17 db242eb1da34
files agent/core/src/main/java/com/redhat/thermostat/backend/BackendID.java agent/core/src/main/java/com/redhat/thermostat/backend/BackendLoadException.java agent/core/src/main/java/com/redhat/thermostat/backend/BackendsProperties.java vm-overview/client-swing/src/main/java/com/redhat/thermostat/vm/overview/client/swing/internal/ChangeableText.java vm-overview/client-swing/src/main/java/com/redhat/thermostat/vm/overview/client/swing/internal/SimpleTable.java
diffstat 44 files changed, 1297 insertions(+), 1149 deletions(-) [+]
line wrap: on
line diff
--- a/agent/core/src/main/java/com/redhat/thermostat/agent/Agent.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/agent/core/src/main/java/com/redhat/thermostat/agent/Agent.java	Fri Feb 22 16:53:10 2013 +0100
@@ -84,38 +84,40 @@
             Backend backend = (Backend) actionEvent.getPayload();
             
             switch (actionEvent.getActionId()) {
-            case SERVICE_ADDED: {
-                // TODO: this backed has been already added, we should
-                // probably signal the user this
-                if (!backendInfos.containsKey(backend)) {
+
+                case SERVICE_ADDED: {
+                    if (!backendInfos.containsKey(backend)) {
+
+                        logger.info("Adding backend: " + backend);
+
+                        backend.activate();
 
-                    logger.info("Adding backend: " + backend);
-                    
-                    backend.activate();
-
-                    BackendInformation info = createBackendInformation(backend);
-                    backendDao.addBackendInformation(info);
-                    backendInfos.put(backend, info);                    
+                        BackendInformation info = createBackendInformation(backend);
+                        backendDao.addBackendInformation(info);
+                        backendInfos.put(backend, info);                    
+                    } else {
+                        logger.warning("Backend registered that agent already knows about:" + backend);
+                    }
+                    break;
                 }
-            }
-            break;
 
-            case SERVICE_REMOVED: {
-                BackendInformation info = backendInfos.get(backend);
-                if (info != null) {
-                    logger.info("removing backend: " + backend);
-                    
-                    backend.deactivate();
-                    
-                    backendDao.removeBackendInformation(info);
-                    backendInfos.remove(backend); 
+                case SERVICE_REMOVED: {
+                    BackendInformation info = backendInfos.get(backend);
+                    if (info != null) {
+                        logger.info("removing backend: " + backend);
+
+                        backend.deactivate();
+
+                        backendDao.removeBackendInformation(info);
+                        backendInfos.remove(backend); 
+                    }
+                    break;
                 }
-            }
-            break;
-                
-            default:
-                logger.log(Level.WARNING, "received unknown event from BackendRegistry: " + actionEvent.getActionId());
-                break;
+
+                default: {
+                    logger.log(Level.WARNING, "received unknown event from BackendRegistry: " + actionEvent.getActionId());
+                    break;
+                }
             }
         }
     };
--- a/agent/core/src/main/java/com/redhat/thermostat/backend/Backend.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/agent/core/src/main/java/com/redhat/thermostat/backend/Backend.java	Fri Feb 22 16:53:10 2013 +0100
@@ -36,130 +36,44 @@
 
 package com.redhat.thermostat.backend;
 
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Map.Entry;
-
 import com.redhat.thermostat.annotations.ExtensionPoint;
-import com.redhat.thermostat.common.LaunchException;
 import com.redhat.thermostat.common.Ordered;
-import com.redhat.thermostat.storage.core.Storage;
 
 /**
  * Represents a plugin that runs on the agent and performs monitoring of host
  * and applications.
  * <p>
- * To register a new backend, register an instance of the class with the OSGi
+ * To register a new backend, register an implementation of this interface with the OSGi
  * service registry.
  * 
+ * Implementations are encouraged to make use of existing abstract helper classes, such
+ * as {@link BaseBackend} or {@link VmListenerBackend}
+ * 
+ * @see BaseBackend
  * @see VmListenerBackend
  */
 @ExtensionPoint
-public abstract class Backend implements Ordered {
-
-    private boolean initialConfigurationComplete = false;
-    private boolean observeNewJvm = attachToNewProcessByDefault();
-
-    private Map<String,String> config = new HashMap<>();
-    
-    private BackendID id;
+public interface Backend extends Ordered {
 
-    public Backend(BackendID id) {
-        this.id = id;
-    }
-    
-    /**
-     * 
-     * @param configMap a map containing the settings that this backend has been configured with.
-     * @throws LaunchException if map contains values that this backend does not accept.
-     */
-    protected final void setInitialConfiguration(Map<String, String> configMap) throws BackendLoadException {
-        if (initialConfigurationComplete) {
-            throw new BackendLoadException(id, "The backend " + id.toString() + "may only receive initial configuration once.");
-        }
-        for (Entry<String, String> e : configMap.entrySet()) {
-            String key = e.getKey();
-            String value = e.getValue();
-            try {
-                setConfigurationValue(key, value);
-            } catch (IllegalArgumentException iae) {
-                throw new BackendLoadException(id, "Attempt to set invalid backend configuration for " + getName()
-                        + " backend.  Key: " + key + "   Value: " + value, iae);
-            }
-        }
-        initialConfigurationComplete = true;
-    }
-
-    /**
-     * Set the named configuration to the given value.
-     * The basic special properties {@code name}, {@code version} and
-     * {@code description} are parsed here.
-     * 
-     * <br /><br />
-     * 
-     * Subclasses can just override the
-     * {@link #setConfigurationValueImpl(String, String)}
-     * method if they are not interested in parsing and setting those
-     * properties directly.
-     * 
-     * @param name
-     * @param value
-     * @throws IllegalArgumentException if either the key does not refer to a valid configuration option
-     *                                  for this backend or the value is not valid for the key
-     */
-    protected void setConfigurationValue(String name, String value) {
-        config.put(name, value);
-    }
-    
     /**
      * @return the name of the {@link Backend}
      */
-    public String getName() {
-        return id.getSimpleName();
-    }
+    public String getName();
 
     /**
      * @returns the description of the {@link Backend}
      */
-    public String getDescription() {
-        return config.get(BackendsProperties.DESCRIPTION.name());
-    }
+    public String getDescription();
 
     /**
      * @return the vendor of the {@link Backend}
      */
-    public String getVendor() {
-        return config.get(BackendsProperties.VENDOR.name());
-    }
+    public String getVendor();
 
     /** 
      * @return the version of the {@link Backend}
      */
-    public String getVersion() {
-        return config.get(BackendsProperties.VERSION.name());
-    }
-
-    /** Get a map containing the current settings of this backend.
-     * Implementors of this abstract class which have some settings that
-     * are be configurable by the client must override this method
-     * to provide an appropriate map.
-     * 
-     * @return a map containing the settings of this backend
-     */
-    public Map<String, String> getConfigurationMap() {
-        return new HashMap<String, String>();
-    }
-
-    /**
-     * 
-     * @param key The constant key that corresponds to the desired configuration value
-     * @return The current value of the configuration value corresponding to the key given.
-     * @throws IllegalArgumentException if the key does not refer to a valid configuration option for
-     *                                  this backend
-     */
-    public String getConfigurationValue(String key) {
-        return config.get(key);
-    }
+    public String getVersion();
 
     /**
      * Activate the {@link Backend}.  Based on the current configuration,
@@ -175,7 +89,7 @@
      * @return {@code true} if the backend was activated successfully or
      * already active. {@code false} if there was an error
      */
-    public abstract boolean activate();
+    public boolean activate();
 
     /**
      * Deactivate the {@link Backend}. The backend should release any
@@ -191,89 +105,25 @@
      * @return {@code true} if the backend was successfully deactivated or
      * already inactive. {@code false} if the backend is still active.
      */
-    public abstract boolean deactivate();
+    public boolean deactivate();
 
     /**
      * @return a boolean indicating whether the backend is currently active on this host
      */
-    public abstract boolean isActive();
-
-    /**
-     * A {@link Backend} may be configured to automatically begin collecting from new Java
-     * processes.  This method determines whether this will be the case when the backend
-     * is initially started.
-     * 
-     * @return true if the initial backend behaviour is to attach to new java processes, false otherwise.
-     */
-    public abstract boolean attachToNewProcessByDefault();
+    public boolean isActive();
 
     /**
      * Indicate whether this backend will attach to new java processes.
      * 
      * @return true if this backend will attach to new java processes, false otherwise.
      */
-    public boolean getObserveNewJvm() {
-        return observeNewJvm;
-    }
+    public boolean getObserveNewJvm();
 
     /**
      * Set whether this backend will attach to new java processes.
      * 
      * @param newValue
      */
-    public void setObserveNewJvm(boolean newValue) {
-        observeNewJvm = newValue;
-    }
-    
-    public BackendID getID() {
-        return id;
-    }
-    
-    @Override
-    public int hashCode() {
-        String vendor = getVendor();
-        String version = getVersion();
-        final int prime = 31;
-        int result = 1;
-        result = prime * result + ((id == null) ? 0 : id.hashCode());
-        result = prime * result + ((vendor == null) ? 0 : vendor.hashCode());
-        result = prime * result + ((version == null) ? 0 : version.hashCode());
-        return result;
-    }
+    public void setObserveNewJvm(boolean newValue);
 
-    @Override
-    public boolean equals(Object obj) {
-        String vendor = getVendor();
-        String version = getVersion();
-        if (this == obj)
-            return true;
-        if (obj == null)
-            return false;
-        if (getClass() != obj.getClass())
-            return false;
-        Backend other = (Backend) obj;
-        if (id == null) {
-            if (other.id != null)
-                return false;
-        } else if (!id.equals(other.id))
-            return false;
-        if (vendor == null) {
-            if (other.getVendor() != null)
-                return false;
-        } else if (!vendor.equals(other.getVendor()))
-            return false;
-        if (version == null) {
-            if (other.getVersion() != null)
-                return false;
-        } else if (!version.equals(other.getVersion()))
-            return false;
-        return true;
-    }
-
-    @Override
-    public String toString() {
-        return "Backend [version=" + getVersion() + ", vendor=" + getVendor()
-                + ", description=" + getDescription() + ", id=" + id + "]";
-    }
 }
-
--- a/agent/core/src/main/java/com/redhat/thermostat/backend/BackendID.java	Fri Feb 22 16:52:26 2013 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,98 +0,0 @@
-/*
- * Copyright 2012, 2013 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.backend;
-
-/**
- * The unique identifier that identifies a backend.
- */
-public class BackendID {
-
-    private final String simpleName;
-    private final String className;
-    
-    public BackendID(String simpleName, String className) {
-        this.simpleName = simpleName;
-        this.className = className;
-    }
-    
-    public String getSimpleName() {
-        return simpleName;
-    }
-    
-    public String getClassName() {
-        return className;
-    }
-    
-    @Override
-    public String toString() {
-        return simpleName + " = " + className;
-    }
-
-    @Override
-    public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        result = prime * result
-                + ((className == null) ? 0 : className.hashCode());
-        result = prime * result
-                + ((simpleName == null) ? 0 : simpleName.hashCode());
-        return result;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj)
-            return true;
-        if (obj == null)
-            return false;
-        if (getClass() != obj.getClass())
-            return false;
-        BackendID other = (BackendID) obj;
-        if (className == null) {
-            if (other.className != null)
-                return false;
-        } else if (!className.equals(other.className))
-            return false;
-        if (simpleName == null) {
-            if (other.simpleName != null)
-                return false;
-        } else if (!simpleName.equals(other.simpleName))
-            return false;
-        return true;
-    }
-}
-
--- a/agent/core/src/main/java/com/redhat/thermostat/backend/BackendLoadException.java	Fri Feb 22 16:52:26 2013 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-/*
- * Copyright 2012, 2013 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.backend;
-
-/**
- * An error occurred when loading or initializing a backend
- */
-public class BackendLoadException extends Exception {
-
-    private static final long serialVersionUID = 4057881401012295723L;
-
-    private final BackendID backendId;
-
-    public BackendLoadException(BackendID backendId, String message) {
-        super(message);
-        this.backendId = backendId;
-    }
-
-    public BackendLoadException(BackendID backendId, String message, Throwable cause) {
-        super(message, cause);
-        this.backendId = backendId;
-    }
-
-    public BackendID getBackend() {
-        return backendId;
-    }
-}
-
--- a/agent/core/src/main/java/com/redhat/thermostat/backend/BackendRegistry.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/agent/core/src/main/java/com/redhat/thermostat/backend/BackendRegistry.java	Fri Feb 22 16:53:10 2013 +0100
@@ -36,14 +36,11 @@
  
 package com.redhat.thermostat.backend;
 
-import java.util.logging.Logger;
-
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.Constants;
 import org.osgi.framework.InvalidSyntaxException;
 
 import com.redhat.thermostat.common.ThermostatExtensionRegistry;
-import com.redhat.thermostat.common.utils.LoggingUtils;
 
 /**
  * A registry for {@link Backend}s. {@link Backend}s are responsible to be
@@ -51,8 +48,6 @@
  */
 public class BackendRegistry extends ThermostatExtensionRegistry<Backend> {
 
-    private static final Logger logger = LoggingUtils.getLogger(BackendRegistry.class);
-
     private static final String FILTER = "(" + Constants.OBJECTCLASS + "=" + Backend.class.getName() + ")";
 
     public BackendRegistry(BundleContext context) throws InvalidSyntaxException {
--- a/agent/core/src/main/java/com/redhat/thermostat/backend/BackendsProperties.java	Fri Feb 22 16:52:26 2013 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-/*
- * Copyright 2012, 2013 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.backend;
-
-/**
- * Properties that any Backend needs to have, at minimum.
- */
-public enum BackendsProperties {
-   
-    // FIXME: Get this info from Bundle metadata and remove this
-    // enum.
-    DESCRIPTION,
-    VENDOR,
-    VERSION;
-    
-}
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/agent/core/src/main/java/com/redhat/thermostat/backend/BaseBackend.java	Fri Feb 22 16:53:10 2013 +0100
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2012, 2013 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.backend;
+
+import java.util.Objects;
+
+public abstract class BaseBackend implements Backend {
+
+    private boolean observeNewJvm;
+    
+    private String name, description, vendor, version;
+
+    public BaseBackend(String name, String description, String vendor, String version) {
+        this(name, description, vendor, version, false);
+    }
+
+    public BaseBackend(String name, String description, String vendor, String version, boolean observeNewJvm) {
+        this.name = Objects.requireNonNull(name);
+        this.description = Objects.requireNonNull(description);
+        this.vendor = Objects.requireNonNull(vendor);
+        this.version = Objects.requireNonNull(version);
+        this.observeNewJvm = observeNewJvm;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public String getDescription() {
+        return description;
+    }
+
+    @Override
+    public String getVendor() {
+        return vendor;
+    }
+
+    @Override
+    public String getVersion() {
+        return version;
+    }
+
+    @Override
+    public boolean getObserveNewJvm() {
+        return observeNewJvm;
+    }
+
+    @Override
+    public void setObserveNewJvm(boolean newValue) {
+        observeNewJvm = newValue;
+    }
+    
+    @Override
+    public int hashCode() {
+        return Objects.hash(name, vendor, version);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        BaseBackend other = (BaseBackend) obj;
+        return Objects.equals(name, other.name) &&
+                Objects.equals(version, other.version) &&
+                Objects.equals(vendor, other.vendor);
+    }
+
+    @Override
+    public String toString() {
+        return "Backend [name=" + getName() + ", version=" + getVersion() + ", vendor=" + getVendor()
+                + ", description=" + getDescription() + "]";
+    }
+}
+
--- a/agent/core/src/main/java/com/redhat/thermostat/backend/VmListenerBackend.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/agent/core/src/main/java/com/redhat/thermostat/backend/VmListenerBackend.java	Fri Feb 22 16:53:10 2013 +0100
@@ -55,14 +55,15 @@
 import com.redhat.thermostat.common.utils.LoggingUtils;
 
 /**
- * This class is a convenient subclass of {@link Backend} for those that need to
- * attach {@link VmListener} in response to starting and stopping of JVMs on a
+ * This class is a convenient subclass of {@link Backend} (via {@link BaseBackend}) for those
+ * that need to attach {@link VmListener} in response to starting and stopping of JVMs on a
  * host.
  * 
  * @see VmStatusListener
  * @see Backend
+ * @see BaseBackend
  */
-public abstract class VmListenerBackend extends Backend implements VmStatusListener {
+public abstract class VmListenerBackend extends BaseBackend implements VmStatusListener {
     
     private static final Logger logger = LoggingUtils.getLogger(VmListenerBackend.class);
 
@@ -71,10 +72,15 @@
     private final VmStatusListenerRegistrar registrar;
     private boolean started;
 
-    public VmListenerBackend(BackendID id, VmStatusListenerRegistrar registrar) {
-        super(id);
+    public VmListenerBackend(String backendName, String description,
+            String vendor, String version, VmStatusListenerRegistrar registrar) {
+        this(backendName, description, vendor, version, false, registrar);
+    }
+    public VmListenerBackend(String backendName, String description,
+            String vendor, String version, boolean observeNewJvm, VmStatusListenerRegistrar registrar) {
+        super(backendName, description, vendor, version, observeNewJvm);
         this.registrar = registrar;
-        
+
         try {
             HostIdentifier hostId = new HostIdentifier((String) null);
             host = MonitoredHost.getMonitoredHost(hostId);
@@ -141,7 +147,7 @@
     }
 
     private void handleNewVm(int pid) {
-        if (attachToNewProcessByDefault()) {
+        if (getObserveNewJvm()) {
             try {
                 MonitoredVm vm = host.getMonitoredVm(host.getHostIdentifier().resolve(new VmIdentifier(String.valueOf(pid))));
                 VmListener listener = createVmListener(pid);
--- a/agent/core/src/test/java/com/redhat/thermostat/agent/AgentTest.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/agent/core/src/test/java/com/redhat/thermostat/agent/AgentTest.java	Fri Feb 22 16:53:10 2013 +0100
@@ -48,9 +48,6 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import java.util.ArrayList;
-import java.util.Collection;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
@@ -93,8 +90,6 @@
         when(backend.getDescription()).thenReturn("testdesc");
         when(backend.getObserveNewJvm()).thenReturn(true);
         when(backend.activate()).thenReturn(true); // TODO: activate() should not return anything and throw exception in error case.
-        Collection<Backend> backends = new ArrayList<Backend>();
-        backends.add(backend);
         
         backendRegistry = mock(BackendRegistry.class);
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/agent/core/src/test/java/com/redhat/thermostat/backend/BaseBackendTest.java	Fri Feb 22 16:53:10 2013 +0100
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2012, 2013 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.backend;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.not;
+
+import org.junit.Test;
+
+public class BaseBackendTest {
+
+    @Test(expected = NullPointerException.class)
+    public void testConstructorRejectsNullName() {
+        new TestBaseBackend(null, "", "", "");
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testConstructorRejectsNullVendor() {
+        new TestBaseBackend("", "", null, "");
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testConstructorRejectsNullVersion() {
+        new TestBaseBackend("", "", "", null);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testConstructorRejectsNullDescription() {
+        new TestBaseBackend("", null, "", "");
+    }
+
+    @Test
+    public void testConvenienceConstructorDefaultsFalseObserveNewJvm() {
+        Backend backend = new TestBaseBackend("", "", "", "");
+        assertFalse(backend.getObserveNewJvm());
+    }
+
+    @Test
+    public void testConstructorValuesWorkAsExpected() {
+        String name = "name";
+        String description = "description";
+        String vendor = "vendor";
+        String version = "version";
+        Backend backend = new TestBaseBackend(name, description, vendor, version, true);
+        assertEquals(name, backend.getName());
+        assertEquals(description, backend.getDescription());
+        assertEquals(vendor, backend.getVendor());
+        assertEquals(version, backend.getVersion());
+        assertTrue(backend.getObserveNewJvm());
+    }
+
+    @Test
+    public void testSetObserveNewJvm() {
+        Backend backend = new TestBaseBackend("name", "description", "vendor", "version");
+        assertFalse(backend.getObserveNewJvm());
+        backend.setObserveNewJvm(true);
+        assertTrue(backend.getObserveNewJvm());
+        backend.setObserveNewJvm(false);
+        assertFalse(backend.getObserveNewJvm());
+    }
+
+    @Test
+    public void testEquals() {
+        TestBaseBackend backend = new TestBaseBackend("name", "description", "vendor", "version");
+        assertEquals(backend, new TestBaseBackend("name", "description", "vendor", "version"));
+
+        // Uniquely identified by name/vendor/version, desc and other properties shouldn't matter.
+        assertEquals(backend, new TestBaseBackend("name", "different description", "vendor", "version"));
+        assertEquals(backend, new TestBaseBackend("name", "description", "vendor", "version", true));
+
+        // Any one of name/vendor/version should not match.
+        assertThat(backend, not(equalTo(new TestBaseBackend("another name", "description", "vendor", "version"))));
+        assertThat(backend, not(equalTo(new TestBaseBackend("name", "description", "other vendor", "version"))));
+        assertThat(backend, not(equalTo(new TestBaseBackend("name", "description", "vendor", "newer version"))));
+    }
+
+    @Test
+    public void testHashcodeReturnsSameForEqualObjects() {
+        TestBaseBackend backend1 = new TestBaseBackend("name", "description", "vendor", "version");
+        TestBaseBackend backend2 = new TestBaseBackend("name", "description", "vendor", "version");
+        assertEquals(backend1, backend2);
+        assertEquals(backend1.hashCode(), backend2.hashCode());
+    }
+
+    @Test
+    public void testHashcodeReturnsSameForSubsequentCalls() {
+        TestBaseBackend backend = new TestBaseBackend("name", "description", "vendor", "version");
+        assertEquals(backend.hashCode(), backend.hashCode());
+    }
+
+    @Test
+    public void testToString() {
+        TestBaseBackend backend = new TestBaseBackend("name", "description", "vendor", "version");
+        assertEquals(backend.toString(), "Backend [name=name, version=version, vendor=vendor, description=description]");
+    }
+
+    /*
+     * Just some passthrough constructors and trivial implementations of abstract methods (not tested here).
+     */
+    private class TestBaseBackend extends BaseBackend {
+
+        public TestBaseBackend(String name, String description, String vendor,
+                String version) {
+            super(name, description, vendor, version);
+        }
+
+        public TestBaseBackend(String name, String description, String vendor,
+                String version, boolean observeNewJvm) {
+            super(name, description, vendor, version, observeNewJvm);
+        }
+
+        @Override
+        public boolean activate() {
+            return false;
+        }
+
+        @Override
+        public boolean deactivate() {
+            return false;
+        }
+
+        @Override
+        public boolean isActive() {
+            return false;
+        }
+
+        @Override
+        public int getOrderValue() {
+            return 0;
+        }
+        
+    }
+}
--- a/agent/core/src/test/java/com/redhat/thermostat/backend/VmListenerBackendTest.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/agent/core/src/test/java/com/redhat/thermostat/backend/VmListenerBackendTest.java	Fri Feb 22 16:53:10 2013 +0100
@@ -39,8 +39,10 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.isA;
+import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
@@ -73,7 +75,6 @@
 
     @Before
     public void setup() throws URISyntaxException, MonitorException {
-        BackendID id = mock(BackendID.class);
         registrar = mock(VmStatusListenerRegistrar.class);
         listener = mock(VmListener.class);
         
@@ -89,7 +90,8 @@
         
         monitoredVm = mock(MonitoredVm.class);
         
-        backend = new TestBackend(id, registrar);
+        backend = new TestBackend("Test Backend", "Backend for test", "Test Co.",
+                "0.0.0", registrar);
         backend.setHost(host);
     }
     
@@ -140,8 +142,13 @@
         VmIdentifier VM_ID = new VmIdentifier(String.valueOf(VM_PID));
         when(host.getMonitoredVm(VM_ID)).thenReturn(monitoredVm);
 
+        // Should be no response if not observing new jvm.
+        backend.setObserveNewJvm(false);
         backend.vmStatusChanged(Status.VM_STARTED, 1);
+        verify(monitoredVm, times(0)).addVmListener(any(VmListener.class));
 
+        backend.setObserveNewJvm(true);
+        backend.vmStatusChanged(Status.VM_STARTED, 1);
         verify(monitoredVm).addVmListener(listener);
     }
 
@@ -151,6 +158,7 @@
         VmIdentifier VM_ID = new VmIdentifier(String.valueOf(VM_PID));
         when(host.getMonitoredVm(VM_ID)).thenReturn(monitoredVm);
 
+        backend.setObserveNewJvm(true);
         backend.vmStatusChanged(Status.VM_ACTIVE, 1);
 
         verify(monitoredVm).addVmListener(listener);
@@ -172,6 +180,7 @@
         VmIdentifier VM_ID = new VmIdentifier(String.valueOf(VM_PID));
         when(host.getMonitoredVm(VM_ID)).thenReturn(monitoredVm);
 
+        backend.setObserveNewJvm(true);
         backend.vmStatusChanged(Status.VM_STARTED, 1);
         backend.vmStatusChanged(Status.VM_STOPPED, 1);
 
@@ -197,6 +206,7 @@
         MonitorException monitorException = new MonitorException();
         doThrow(monitorException).when(monitoredVm).removeVmListener(listener);
 
+        backend.setObserveNewJvm(true);
         backend.vmStatusChanged(Status.VM_STARTED, 1);
         backend.vmStatusChanged(Status.VM_STOPPED, 1);
 
@@ -211,6 +221,7 @@
         VmIdentifier VM_ID = new VmIdentifier(String.valueOf(VM_PID));
         when(host.getMonitoredVm(VM_ID)).thenReturn(monitoredVm);
 
+        backend.setObserveNewJvm(true);
         backend.vmStatusChanged(Status.VM_STARTED, 1);
         backend.deactivate();
         verify(monitoredVm).removeVmListener(listener);
@@ -218,8 +229,9 @@
     
     private class TestBackend extends VmListenerBackend {
 
-        public TestBackend(BackendID id, VmStatusListenerRegistrar registrar) {
-            super(id, registrar);
+        public TestBackend(String name, String description, String vendor,
+                String version, VmStatusListenerRegistrar registrar) {
+            super(name, description, vendor, version, registrar);
         }
 
         @Override
@@ -231,11 +243,6 @@
         protected VmListener createVmListener(int pid) {
             return listener;
         }
-
-        @Override
-        public boolean attachToNewProcessByDefault() {
-            return true;
-        }
         
     }
 
--- a/client/cli/src/main/java/com/redhat/thermostat/client/cli/internal/ShellCommand.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/client/cli/src/main/java/com/redhat/thermostat/client/cli/internal/ShellCommand.java	Fri Feb 22 16:53:10 2013 +0100
@@ -53,6 +53,7 @@
 import org.osgi.framework.ServiceReference;
 
 import com.redhat.thermostat.common.Launcher;
+import com.redhat.thermostat.common.Version;
 import com.redhat.thermostat.common.cli.AbstractCommand;
 import com.redhat.thermostat.common.cli.CommandContext;
 import com.redhat.thermostat.common.cli.CommandException;
@@ -72,6 +73,7 @@
     private static final String PROMPT = "Thermostat > ";
 
     private HistoryProvider historyProvider;
+    private Version version;
 
     private BundleContext bundleContext;
     
@@ -88,12 +90,13 @@
     }
 
     public ShellCommand() {
-        this(FrameworkUtil.getBundle(ShellCommand.class).getBundleContext(), new HistoryProvider());
+        this(FrameworkUtil.getBundle(ShellCommand.class).getBundleContext(), new Version(), new HistoryProvider());
     }
 
-    ShellCommand(BundleContext context, HistoryProvider provider) {
+    ShellCommand(BundleContext context, Version version, HistoryProvider provider) {
         this.historyProvider = provider;
         this.bundleContext = context;
+        this.version = version;
     }
     
     @Override
@@ -102,6 +105,7 @@
         PersistentHistory history = historyProvider.get();
 
         try {
+            ctx.getConsole().getOutput().println(version.getVersionInfo());
             shellMainLoop(ctx, history, term);
         } catch (IOException ex) {
             throw new CommandException(ex);
--- a/client/cli/src/test/java/com/redhat/thermostat/client/cli/internal/ShellCommandTest.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/client/cli/src/test/java/com/redhat/thermostat/client/cli/internal/ShellCommandTest.java	Fri Feb 22 16:53:10 2013 +0100
@@ -60,6 +60,7 @@
 
 import com.redhat.thermostat.client.cli.internal.ShellCommand.HistoryProvider;
 import com.redhat.thermostat.common.Launcher;
+import com.redhat.thermostat.common.Version;
 import com.redhat.thermostat.common.cli.Arguments;
 import com.redhat.thermostat.common.cli.CommandContext;
 import com.redhat.thermostat.common.cli.CommandException;
@@ -68,13 +69,23 @@
 
 public class ShellCommandTest {
 
+    static private final String VERSION = "Thermostat some version";
+    static private final String VERSION_OUTPUT = VERSION + "\n";
+
     private ShellCommand cmd;
+
     private BundleContext bundleContext;
+    private HistoryProvider historyProvider;
+    private Version version;
 
     @Before
     public void setUp() {
         bundleContext = mock(BundleContext.class);
-        cmd = new ShellCommand(bundleContext, new HistoryProvider());
+        historyProvider = mock(HistoryProvider.class);
+        version = mock(Version.class);
+        when(version.getVersionInfo()).thenReturn(VERSION);
+
+        cmd = new ShellCommand(bundleContext, version, historyProvider);
     }
 
     @After
@@ -108,7 +119,7 @@
         Arguments args = new SimpleArguments();
         CommandContext ctx = ctxFactory.createContext(args);
         cmd.run(ctx);
-        assertEquals("Thermostat > quit\n", ctxFactory.getOutput());
+        assertEquals(VERSION_OUTPUT + "Thermostat > quit\n", ctxFactory.getOutput());
         assertEquals("", ctxFactory.getError());
     }
 
@@ -119,7 +130,7 @@
         Arguments args = new SimpleArguments();
         CommandContext ctx = ctxFactory.createContext(args);
         cmd.run(ctx);
-        assertEquals("Thermostat > q\n", ctxFactory.getOutput());
+        assertEquals(VERSION_OUTPUT + "Thermostat > q\n", ctxFactory.getOutput());
         assertEquals("", ctxFactory.getError());
     }
 
@@ -130,7 +141,7 @@
         Arguments args = new SimpleArguments();
         CommandContext ctx = ctxFactory.createContext(args);
         cmd.run(ctx);
-        assertEquals("Thermostat > ", ctxFactory.getOutput());
+        assertEquals(VERSION_OUTPUT + "Thermostat > ", ctxFactory.getOutput());
         assertEquals("", ctxFactory.getError());
     }
 
@@ -141,7 +152,7 @@
         Arguments args = new SimpleArguments();
         CommandContext ctx = ctxFactory.createContext(args);
         cmd.run(ctx);
-        assertEquals("Thermostat > \nThermostat > exit\n", ctxFactory.getOutput());
+        assertEquals(VERSION_OUTPUT + "Thermostat > \nThermostat > exit\n", ctxFactory.getOutput());
     }
 
     @Test
@@ -150,8 +161,7 @@
         when(history.previous()).thenReturn(true);
         when(history.current()).thenReturn("old-history-value");
 
-        HistoryProvider provider = mock(HistoryProvider.class);
-        when(provider.get()).thenReturn(history);
+        when(historyProvider.get()).thenReturn(history);
 
         ServiceReference ref = mock(ServiceReference.class);
         
@@ -160,15 +170,13 @@
         when(bundleContext.getService(ref)).thenReturn(launcher);
         TestCommandContextFactory ctxFactory = new TestCommandContextFactory(bundleContext);
 
-        cmd = new ShellCommand(bundleContext, provider);
-        
         // "\u001b[A" is the escape code for up-arrow. use xxd -p to generate
         ctxFactory.setInput("\u001b[A\nexit\n");
         Arguments args = new SimpleArguments();
         CommandContext ctx = ctxFactory.createContext(args);
         cmd.run(ctx);
 
-        assertEquals("Thermostat > old-history-value\nThermostat > exit\n", ctxFactory.getOutput());
+        assertEquals(VERSION_OUTPUT + "Thermostat > old-history-value\nThermostat > exit\n", ctxFactory.getOutput());
         assertEquals("", ctxFactory.getError());
 
         verify(launcher).setArgs(new String[] {"old-history-value"});
@@ -178,8 +186,7 @@
     @Test
     public void testHistoryIsUpdated() throws CommandException, IOException {
         PersistentHistory mockHistory = mock(PersistentHistory.class);
-        HistoryProvider provider = mock(HistoryProvider.class);
-        when(provider.get()).thenReturn(mockHistory);
+        when(historyProvider.get()).thenReturn(mockHistory);
 
         ServiceReference ref = mock(ServiceReference.class);
         when(bundleContext.getServiceReference(Launcher.class.getName())).thenReturn(ref);
@@ -187,8 +194,6 @@
         when(bundleContext.getService(ref)).thenReturn(launcher);
         TestCommandContextFactory ctxFactory = new TestCommandContextFactory(bundleContext);
         
-        cmd = new ShellCommand(bundleContext, provider);
-        
         ctxFactory.setInput("add-to-history\nexit\n");
         Arguments args = new SimpleArguments();
         CommandContext ctx = ctxFactory.createContext(args);
@@ -199,7 +204,7 @@
         verify(mockHistory).add("add-to-history");
         verify(mockHistory).flush();
 
-        assertEquals("Thermostat > add-to-history\nThermostat > exit\n", ctxFactory.getOutput());
+        assertEquals(VERSION_OUTPUT + "Thermostat > add-to-history\nThermostat > exit\n", ctxFactory.getOutput());
         assertEquals("", ctxFactory.getError());
     }
 
--- a/distribution/config/commands/ping.properties	Fri Feb 22 16:52:26 2013 +0100
+++ b/distribution/config/commands/ping.properties	Fri Feb 22 16:53:10 2013 +0100
@@ -8,6 +8,7 @@
           gson.jar, \
           mongo.jar, \
           commons-beanutils.jar, \
+          commons-codec.jar, \
           commons-collections.jar, \
           commons-logging.jar, \
           netty.jar
--- a/host-cpu/agent/src/main/java/com/redhat/thermostat/host/cpu/agent/internal/HostCpuBackend.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/host-cpu/agent/src/main/java/com/redhat/thermostat/host/cpu/agent/internal/HostCpuBackend.java	Fri Feb 22 16:53:10 2013 +0100
@@ -39,9 +39,7 @@
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 
-import com.redhat.thermostat.backend.Backend;
-import com.redhat.thermostat.backend.BackendID;
-import com.redhat.thermostat.backend.BackendsProperties;
+import com.redhat.thermostat.backend.BaseBackend;
 import com.redhat.thermostat.common.Clock;
 import com.redhat.thermostat.common.SystemClock;
 import com.redhat.thermostat.common.Version;
@@ -49,7 +47,7 @@
 import com.redhat.thermostat.utils.ProcDataSource;
 import com.redhat.thermostat.utils.SysConf;
 
-public class HostCpuBackend extends Backend {
+public class HostCpuBackend extends BaseBackend {
 
     private static final long PROC_CHECK_INTERVAL = 1000; // TODO make this configurable.
     
@@ -59,14 +57,12 @@
     private boolean started;
 
     public HostCpuBackend(ScheduledExecutorService executor, CpuStatDAO cpuStatDAO, Version version) {
-        super(new BackendID("Host CPU Backend", HostCpuBackend.class.getName()));
+        super("Host CPU Backend",
+                "Gathers CPU statistics about a host",
+                "Red Hat, Inc.",
+                version.getVersionNumber(), true);
         this.executor = executor;
         this.cpuStats = cpuStatDAO;
-        
-        setConfigurationValue(BackendsProperties.VENDOR.name(), "Red Hat, Inc.");
-        setConfigurationValue(BackendsProperties.DESCRIPTION.name(), "Gathers CPU statistics about a host");
-        setConfigurationValue(BackendsProperties.VERSION.name(), version.getVersionNumber());
-        
         Clock clock = new SystemClock();
         long ticksPerSecond = SysConf.getClockTicksPerSecond();
         ProcDataSource source = new ProcDataSource();
@@ -104,16 +100,6 @@
     }
 
     @Override
-    public String getConfigurationValue(String key) {
-        return null;
-    }
-
-    @Override
-    public boolean attachToNewProcessByDefault() {
-        return true;
-    }
-
-    @Override
     public int getOrderValue() {
         return ORDER_CPU_GROUP;
     }
--- a/host-memory/agent/src/main/java/com/redhat/thermostat/host/memory/agent/internal/HostMemoryBackend.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/host-memory/agent/src/main/java/com/redhat/thermostat/host/memory/agent/internal/HostMemoryBackend.java	Fri Feb 22 16:53:10 2013 +0100
@@ -39,14 +39,12 @@
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 
-import com.redhat.thermostat.backend.Backend;
-import com.redhat.thermostat.backend.BackendID;
-import com.redhat.thermostat.backend.BackendsProperties;
+import com.redhat.thermostat.backend.BaseBackend;
 import com.redhat.thermostat.common.Version;
 import com.redhat.thermostat.host.memory.common.MemoryStatDAO;
 import com.redhat.thermostat.utils.ProcDataSource;
 
-public class HostMemoryBackend extends Backend {
+public class HostMemoryBackend extends BaseBackend {
 
     private static final long PROC_CHECK_INTERVAL = 1000; // TODO make this configurable.
     
@@ -56,14 +54,13 @@
     private boolean started;
 
     public HostMemoryBackend(ScheduledExecutorService executor, MemoryStatDAO memoryStatDAO, Version version) {
-        super(new BackendID("Host Memory Backend", HostMemoryBackend.class.getName()));
+        super("Host Memory Backend",
+                "Gathers memory statistics about a host",
+                "Red Hat, Inc.",
+                version.getVersionNumber(), true);
         this.executor = executor;
         this.memoryStats = memoryStatDAO;
-        
-        setConfigurationValue(BackendsProperties.VENDOR.name(), "Red Hat, Inc.");
-        setConfigurationValue(BackendsProperties.DESCRIPTION.name(), "Gathers memory statistics about a host");
-        setConfigurationValue(BackendsProperties.VERSION.name(), version.getVersionNumber());
-        
+
         ProcDataSource source = new ProcDataSource();
         memoryStatBuilder = new MemoryStatBuilder(source);
     }
@@ -95,16 +92,6 @@
     }
 
     @Override
-    public String getConfigurationValue(String key) {
-        return null;
-    }
-
-    @Override
-    public boolean attachToNewProcessByDefault() {
-        return true;
-    }
-
-    @Override
     public int getOrderValue() {
         return ORDER_MEMORY_GROUP;
     }
--- a/integration-tests/pom.xml	Fri Feb 22 16:52:26 2013 +0100
+++ b/integration-tests/pom.xml	Fri Feb 22 16:53:10 2013 +0100
@@ -57,6 +57,30 @@
 
   <build>
     <plugins>
+      <!-- jacoco:report insists to have a target/classes dir. since this
+           module only contains test classes it won't exist after a build.
+           Hence, create manually in order to unbreak the build. For some
+           reason skipping both, report AND prepare-agent goals for the
+           jacoco plugin does not work. -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-antrun-plugin</artifactId>
+        <version>1.6</version>
+        <executions>
+          <execution>
+            <id>make-target-classes-dir</id>
+            <phase>prepare-package</phase>
+            <configuration>
+              <target>
+                <mkdir dir="${project.build.directory}/classes" />
+              </target>
+            </configuration>
+            <goals>
+              <goal>run</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
       <!-- skip unit test run, tests to be executed during integration-test -->
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
@@ -96,6 +120,39 @@
         </executions>
       </plugin>
     </plugins>
+    <pluginManagement>
+    	<plugins>
+		<!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself.-->
+    		<plugin>
+    			<groupId>org.eclipse.m2e</groupId>
+    			<artifactId>lifecycle-mapping</artifactId>
+    			<version>1.0.0</version>
+    			<configuration>
+    				<lifecycleMappingMetadata>
+    					<pluginExecutions>
+    						<pluginExecution>
+    							<pluginExecutionFilter>
+    								<groupId>
+    									org.apache.maven.plugins
+    								</groupId>
+    								<artifactId>
+    									maven-dependency-plugin
+    								</artifactId>
+    								<versionRange>[2.4,)</versionRange>
+    								<goals>
+    									<goal>copy-dependencies</goal>
+    								</goals>
+    							</pluginExecutionFilter>
+    							<action>
+    								<ignore></ignore>
+    							</action>
+    						</pluginExecution>
+    					</pluginExecutions>
+    				</lifecycleMappingMetadata>
+    			</configuration>
+    		</plugin>
+    	</plugins>
+    </pluginManagement>
   </build>
   <dependencies>
 
--- a/integration-tests/src/test/java/com/redhat/thermostat/itest/CliTest.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/integration-tests/src/test/java/com/redhat/thermostat/itest/CliTest.java	Fri Feb 22 16:53:10 2013 +0100
@@ -37,6 +37,7 @@
 package com.redhat.thermostat.itest;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -135,6 +136,39 @@
     }
 
     @Test
+    public void testShellPrintsVersionOnStartup() throws Exception {
+        Spawn shell = spawnThermostat("shell");
+
+        shell.expect(SHELL_PROMPT);
+
+        String stdOut = shell.getCurrentStandardOutContents();
+        assertTrue(stdOut.contains("Thermostat version "));
+    }
+    
+    @Test
+    public void versionArgumentInShellIsNotAllowed() throws Exception {
+        Spawn shell = spawnThermostat("shell");
+
+        shell.expect(SHELL_PROMPT);
+        shell.send("--version\n");
+
+        shell.expect(SHELL_PROMPT);
+
+        String stdOut = shell.getCurrentStandardOutContents();
+        String stdErr = shell.getCurrentStandardErrContents();
+
+        assertMatchesHelpCommandList(shell.getCurrentStandardOutContents());
+        // use the Pattern.DOTALL flag (?s) so that line terminators match with
+        // ".*". stdOut contains the SHELL_PROMPT too.
+        assertTrue(stdOut.matches("(?s)^.*\nunknown command '--version'\n.*$"));
+        assertEquals(stdErr, "");
+        
+        shell.send("exit\n");
+
+        shell.expectClose();
+    }
+
+    @Test
     public void testShellHelp() throws Exception {
         Spawn shell = spawnThermostat("help", "shell");
         shell.expectClose();
@@ -152,8 +186,13 @@
     @Test
     public void testShellUnrecognizedArgument() throws Exception {
         Spawn shell = spawnThermostat("shell", "--foo");
-        shell.expectErr("Unrecognized option: --foo");
         shell.expectClose();
+        String stdOut = shell.getCurrentStandardOutContents();
+        String expectedOut = "Unrecognized option: --foo\n"
+                           + "usage: thermostat shell\n"
+                           + "                  launches the Thermostat interactive shell\n"
+                           + "thermostat shell\n\n";
+        assertEquals(expectedOut, stdOut);
     }
 
     @Test
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/BasicCommandInfo.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/BasicCommandInfo.java	Fri Feb 22 16:53:10 2013 +0100
@@ -83,4 +83,8 @@
         return resources;
     }
 
+    @Override
+    public String toString() {
+        return String.format("%s (description='%s', dependencies='%s')", name, description, resources.toString());
+    }
 }
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/CompoundCommandInfoSource.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/CompoundCommandInfoSource.java	Fri Feb 22 16:53:10 2013 +0100
@@ -66,8 +66,24 @@
 
     @Override
     public CommandInfo getCommandInfo(String name) throws CommandInfoNotFoundException {
-        CommandInfo info1 = source1.getCommandInfo(name);
-        CommandInfo info2 = source2.getCommandInfo(name);
+        CommandInfo info1;
+        CommandInfo info2;
+
+        try {
+            info1 = source1.getCommandInfo(name);
+        } catch (CommandInfoNotFoundException notFound) {
+            info1 = null;
+        }
+        try {
+            info2 = source2.getCommandInfo(name);
+        } catch (CommandInfoNotFoundException notFound) {
+            info2 = null;
+        }
+
+        if (info1 == null && info2 == null) {
+            throw new CommandInfoNotFoundException(name);
+        }
+
         if (info1 == null) {
             return info2;
         } if (info2 == null) {
@@ -91,10 +107,9 @@
             String cmdName = info.getName();
             if (!result.containsKey(cmdName)) {
                 result.put(cmdName, info);
-                continue;
+            } else {
+                result.put(cmdName, merge(result.get(cmdName), info));
             }
-
-            result.put(cmdName, merge(result.get(cmdName), info));
         }
 
         return result.values();
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/LauncherImpl.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/LauncherImpl.java	Fri Feb 22 16:53:10 2013 +0100
@@ -119,7 +119,7 @@
         try {
             if (hasNoArguments()) {
                 runHelpCommand();
-            } else if (isVersionQuery()) {
+            } else if (isVersionQuery(inShell)) {
                 // We want to print the version of core
                 // thermostat, so we use the no-arg constructor of Version
                 Version coreVersion = new Version();
@@ -243,7 +243,14 @@
             }
         }
         Options options = cmd.getOptions();
-        Arguments args = parseCommandArguments(cmdArgs, options);
+        Arguments args = null;
+        try {
+            args = parseCommandArguments(cmdArgs, options);
+        } catch (CommandLineArgumentParseException e) {
+            out.println(e.getMessage());
+            runHelpCommandFor(cmdName);
+            return;
+        }
         setupLogLevel(args);
         CommandContext ctx = setupCommandContext(cmd, args);
         cmd.run(ctx);
@@ -329,8 +336,13 @@
         return ctx;
     }
 
-    private boolean isVersionQuery() {
-        return args[0].equals(Version.VERSION_OPTION);
+    private boolean isVersionQuery(boolean inShell) {
+        // don't allow --version in the shell
+        if (inShell) {
+            return false;
+        } else {
+            return args[0].equals(Version.VERSION_OPTION);
+        }
     }
 
     static class LoggingInitializer {
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginCommandInfoSource.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginCommandInfoSource.java	Fri Feb 22 16:53:10 2013 +0100
@@ -41,6 +41,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -53,19 +54,32 @@
 import com.redhat.thermostat.common.cli.CommandInfoSource;
 import com.redhat.thermostat.common.utils.LoggingUtils;
 import com.redhat.thermostat.launcher.internal.PluginConfiguration.CommandExtensions;
+import com.redhat.thermostat.launcher.internal.PluginConfiguration.NewCommand;
 
+/**
+ * Searches for plugins under <code>$THERMOSTAT_HOME/plugins/</code> and
+ * provides information about all commands specified by them.
+ * <p>
+ * Each plugin is located under
+ * <code>$THERMOSTAT_HOME/plugins/$PLUGIN_NAME/</code> and must have a
+ * <code>plugin.xml</code> file in the main plugin directory.
+ *
+ * @see PluginConfigurationParser how the plugin.xml file is parsed
+ */
 public class PluginCommandInfoSource implements CommandInfoSource {
 
-    private Logger logger = LoggingUtils.getLogger(PluginCommandInfoSource.class);
+    private static final String PLUGIN_CONFIG_FILE = "plugin.xml";
 
-    private Map<String, List<String>> allInfo = new HashMap<>();
+    private static final Logger logger = LoggingUtils.getLogger(PluginCommandInfoSource.class);
+
+    private Map<String, BasicCommandInfo> allNewCommands = new HashMap<>();
+    private Map<String, List<String>> additionalBundlesForExistingCommands = new HashMap<>();
 
     public PluginCommandInfoSource(String internalJarRoot, String pluginRootDir) {
         this(new File(internalJarRoot), new File(pluginRootDir), new PluginConfigurationParser());
     }
 
-    public PluginCommandInfoSource(File internalJarRoot, File pluginRootDir,
-            PluginConfigurationParser parser) {
+    PluginCommandInfoSource(File internalJarRoot, File pluginRootDir, PluginConfigurationParser parser) {
         File[] pluginDirs = pluginRootDir.listFiles();
         if (pluginDirs == null) {
             logger.log(Level.SEVERE, "plugin root dir " + pluginRootDir + " does not exist");
@@ -74,28 +88,28 @@
 
         for (File pluginDir : pluginDirs) {
             try {
-                File configurationFile = new File(pluginDir, "plugin.conf");
+                File configurationFile = new File(pluginDir, PLUGIN_CONFIG_FILE);
                 PluginConfiguration pluginConfig = parser.parse(configurationFile);
                 loadNewAndExtendedCommands(internalJarRoot, pluginDir, pluginConfig);
             } catch (PluginConfigurationParseException | FileNotFoundException exception) {
                 logger.log(Level.WARNING, "unable to parse plugin configuration", exception);
             }
         }
+
+        combineCommands();
     }
 
     private void loadNewAndExtendedCommands(File coreJarRoot, File pluginDir,
             PluginConfiguration pluginConfig) {
 
-        List<CommandExtensions> allExtensions = pluginConfig.getExtendedCommands();
-
-        for (CommandExtensions extension : allExtensions) {
+        for (CommandExtensions extension : pluginConfig.getExtendedCommands()) {
             String commandName = extension.getCommandName();
-            List<String> pluginBundles = extension.getAdditionalBundles();
+            List<String> pluginBundles = extension.getPluginBundles();
             List<String> dependencyBundles = extension.getDepenedencyBundles();
             logger.config("plugin at " + pluginDir + " contributes " +
                     pluginBundles.size() + " bundles to comamnd '" + commandName + "'");
 
-            List<String> bundlePaths = allInfo.get(commandName);
+            List<String> bundlePaths = additionalBundlesForExistingCommands.get(commandName);
             if (bundlePaths == null) {
                 bundlePaths = new LinkedList<>();
             }
@@ -107,23 +121,73 @@
                 bundlePaths.add(new File(coreJarRoot, bundle).toURI().toString());
             }
 
-            allInfo.put(commandName, bundlePaths);
+            additionalBundlesForExistingCommands.put(commandName, bundlePaths);
+        }
+
+        for (NewCommand command : pluginConfig.getNewCommands()) {
+            String commandName = command.getCommandName();
+            logger.config("plugin at " + pluginDir + " contributes new command '" + commandName + "'");
+
+            if (allNewCommands.containsKey(commandName)) {
+                throw new IllegalStateException("multiple plugins are providing the command " + commandName);
+            }
+
+            List<String> bundlePaths = new LinkedList<>();
+
+            for (String bundle : command.getPluginBundles()) {
+                bundlePaths.add(new File(pluginDir, bundle).toURI().toString());
+            }
+            for (String bundle : command.getDepenedencyBundles()) {
+                bundlePaths.add(new File(coreJarRoot, bundle).toURI().toString());
+            }
+            BasicCommandInfo info = new BasicCommandInfo(commandName,
+                    command.getDescription(),
+                    command.getUsage(),
+                    command.getOptions(),
+                    bundlePaths);
+
+            allNewCommands.put(commandName, info);
+        }
+
+    }
+
+    private void combineCommands() {
+        Iterator<Entry<String, List<String>>> iter = additionalBundlesForExistingCommands.entrySet().iterator();
+        while (iter.hasNext()) {
+            Map.Entry<String, List<String>> entry = iter.next();
+            if (allNewCommands.containsKey(entry.getKey())) {
+                BasicCommandInfo old = allNewCommands.get(entry.getKey());
+                List<String> updatedResources = new ArrayList<>();
+                updatedResources.addAll(old.getDependencyResourceNames());
+                updatedResources.addAll(entry.getValue());
+                BasicCommandInfo updated = new BasicCommandInfo(old.getName(),
+                        old.getDescription(),
+                        old.getUsage(),
+                        old.getOptions(),
+                        updatedResources);
+                allNewCommands.put(entry.getKey(), updated);
+                iter.remove();
+            }
         }
     }
 
     @Override
     public CommandInfo getCommandInfo(String name) throws CommandInfoNotFoundException {
-        List<String> bundles = allInfo.get(name);
-        if (bundles == null) {
-            return null;
+        if (allNewCommands.containsKey(name)) {
+            return allNewCommands.get(name);
         }
-        return new BasicCommandInfo(name, null, null, null, bundles);
+        List<String> bundles = additionalBundlesForExistingCommands.get(name);
+        if (bundles != null) {
+            return new BasicCommandInfo(name, null, null, null, bundles);
+        }
+        throw new CommandInfoNotFoundException(name);
     }
 
     @Override
     public Collection<CommandInfo> getCommandInfos() {
         List<CommandInfo> result = new ArrayList<>();
-        for (Entry<String, List<String>> entry : allInfo.entrySet()) {
+        result.addAll(allNewCommands.values());
+        for (Entry<String, List<String>> entry : additionalBundlesForExistingCommands.entrySet()) {
             result.add(new BasicCommandInfo(entry.getKey(), null, null, null, entry.getValue()));
         }
         return result;
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfiguration.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfiguration.java	Fri Feb 22 16:53:10 2013 +0100
@@ -75,7 +75,7 @@
             return commandName;
         }
 
-        public List<String> getAdditionalBundles() {
+        public List<String> getPluginBundles() {
             return Collections.unmodifiableList(additionalResources);
         }
 
@@ -119,11 +119,11 @@
             return options;
         }
 
-        public List<String> getAdditionalBundles() {
+        public List<String> getPluginBundles() {
             return Collections.unmodifiableList(additionalResources);
         }
 
-        public List<String> getCoreDepenedencyBundles() {
+        public List<String> getDepenedencyBundles() {
             return Collections.unmodifiableList(coreDeps);
         }
     }
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParser.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParser.java	Fri Feb 22 16:53:10 2013 +0100
@@ -42,8 +42,10 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.logging.Logger;
 
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
@@ -56,18 +58,58 @@
 import org.xml.sax.SAXException;
 
 import com.redhat.thermostat.common.Pair;
+import com.redhat.thermostat.common.utils.LoggingUtils;
 import com.redhat.thermostat.launcher.internal.PluginConfiguration.CommandExtensions;
 import com.redhat.thermostat.launcher.internal.PluginConfiguration.NewCommand;
 
+/**
+ * Parses the configuration of a plugin as specified in an {@code File} or an
+ * {@code InputStream}. This configuration describes which new commands this
+ * plugin provides as well as additional jars to load for existing commands.
+ * <p>
+ * A example configuration looks like the following:
+ *
+ * <pre>
+ * &lt;?xml version="1.0"?&gt;
+ * &lt;plugin&gt;
+ *   &lt;commands&gt;
+ *     &lt;command type="extends"&gt;
+ *       &lt;name&gt;gui&lt;/name&gt;
+ *       &lt;bundles&gt;
+ *         &lt;bundle&gt;hello-world-plugin-0.1-SNAPSHOT.jar&lt;/bundle&gt;
+ *       &lt;/bundles&gt;
+ *       &lt;dependencies&gt;
+ *         &lt;dependency&gt;thermostat-client-core-0.6.0-SNAPSHOT.jar&lt;/dependency&gt;
+ *       &lt;/dependencies&gt;
+ *     &lt;/command&gt;
+ *     &lt;command type="provides"&gt;
+ *       &lt;name&gt;hello&lt;/name&gt;
+ *       &lt;description&gt;print hello&lt;/description&gt;
+ *       &lt;usage&gt;hello&lt;/usage&gt;
+ *       &lt;bundles&gt;
+ *         &lt;bundle&gt;hello-world-plugin-0.1-SNAPSHOT.jar&lt;/bundle&gt;
+ *       &lt;/bundles&gt;
+ *       &lt;dependencies&gt;
+ *         &lt;dependency&gt;thermostat-client-core-0.6.0-SNAPSHOT.jar&lt;/dependency&gt;
+ *       &lt;/dependencies&gt;
+ *     &lt;/command&gt;
+ *   &lt;/commands&gt;
+ * &lt;/plugin&gt;
+ * </pre>
+ * <p>
+ * This class is thread-safe
+ */
 public class PluginConfigurationParser {
 
-    // no state :)
+    private static final Logger logger = LoggingUtils.getLogger(PluginConfigurationParser.class);
+
+    // thread safe because there is no state :)
 
     public PluginConfiguration parse(File configurationFile) throws FileNotFoundException {
-        return parse(new FileInputStream(configurationFile));
+        return parse(configurationFile.getParentFile().getName(), new FileInputStream(configurationFile));
     }
 
-    public PluginConfiguration parse(InputStream configurationStream) {
+    public PluginConfiguration parse(String pluginName, InputStream configurationStream) {
         try {
             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
             DocumentBuilder builder = factory.newDocumentBuilder();
@@ -76,13 +118,13 @@
             if (rootNode == null) {
                 throw new PluginConfigurationParseException("no configuration found");
             }
-            return parseRootElement(rootNode);
+            return parseRootElement(pluginName, rootNode);
         } catch (ParserConfigurationException | SAXException | IOException exception) {
             throw new PluginConfigurationParseException("failed to parse plugin configuration", exception);
         }
     }
 
-    private PluginConfiguration parseRootElement(Node root) {
+    private PluginConfiguration parseRootElement(String pluginName, Node root) {
         List<NewCommand> newCommands = Collections.emptyList();
         List<CommandExtensions> extensions = Collections.emptyList();
 
@@ -92,7 +134,7 @@
             for (int i = 0; i < nodes.getLength(); i++) {
                 Node node = nodes.item(i);
                 if (node.getNodeName().equals("commands")) {
-                    commands = parseCommands(node);
+                    commands = parseCommands(pluginName, node);
                 }
             }
         }
@@ -100,22 +142,31 @@
         return new PluginConfiguration(commands.getFirst(), commands.getSecond());
     }
 
-    private Pair<List<NewCommand>, List<CommandExtensions>> parseCommands(Node commandsNode) {
+    private Pair<List<NewCommand>, List<CommandExtensions>> parseCommands(String pluginName, Node commandsNode) {
         List<NewCommand> newCommands = new ArrayList<NewCommand>();
         List<CommandExtensions> extendedCommands = new ArrayList<CommandExtensions>();
         NodeList childNodes = commandsNode.getChildNodes();
         for (int i = 0; i < childNodes.getLength(); i++) {
             Node node = childNodes.item(i);
-            if (node.getNodeName().equals("new")) {
-                newCommands.add(parseNewCommand(node));
-            } else if (node.getNodeName().equals("existing")) {
-                extendedCommands.add(parseAdditionsToExistingCommand(node));
+            if (node.getNodeName().equals("command")) {
+                String type = node.getAttributes().getNamedItem("type").getNodeValue();
+                if (type.equals("extends")) {
+                    CommandExtensions additions = parseAdditionsToExistingCommand(pluginName, node);
+                    if (additions != null) {
+                        extendedCommands.add(additions);
+                    }
+                } else if (type.equals("provides")) {
+                    NewCommand newCmd = parseNewCommand(pluginName, node);
+                    if (newCmd != null) {
+                        newCommands.add(newCmd);
+                    }
+                }
             }
         }
         return new Pair<>(newCommands, extendedCommands);
     }
 
-    private CommandExtensions parseAdditionsToExistingCommand(Node commandNode) {
+    private CommandExtensions parseAdditionsToExistingCommand(String pluginName, Node commandNode) {
         String name = null;
         List<String> bundles = new ArrayList<>();
         List<String> dependencies = new ArrayList<>();
@@ -124,33 +175,25 @@
         for (int i = 0; i < nodes.getLength(); i++) {
             Node node = nodes.item(i);
             if (node.getNodeName().equals("name")) {
-                name = node.getTextContent();
+                name = node.getTextContent().trim();
             } else if (node.getNodeName().equals("bundles")) {
-                String[] bundleNames = node.getTextContent().split(",");
-                for (String bundleName : bundleNames) {
-                    if (bundleName.trim().length() == 0) {
-                        continue;
-                    }
-                    bundles.add(bundleName.trim());
-                }
+                bundles.addAll(parseBundles(node));
             } else if (node.getNodeName().equals("dependencies")) {
-                String[] dependencyNames = node.getTextContent().split(",");
-                for (String bundleName : dependencyNames) {
-                    if (bundleName.trim().length() == 0) {
-                        continue;
-                    }
-                    dependencies.add(bundleName);
-                }
+                dependencies.addAll(parseDependencies(node));
             }
         }
+        if (name == null) {
+            logger.warning("plugin " + pluginName + " provides extensions without specifying the command");
+            return null;
+        }
         return new CommandExtensions(name, bundles, dependencies);
     }
 
-    private NewCommand parseNewCommand(Node commandNode) {
+    private NewCommand parseNewCommand(String pluginName, Node commandNode) {
         String name = null;
         String usage = null;
         String description = null;
-        Options options = null;
+        Options options = new Options();
         List<String> bundles = new ArrayList<>();
         List<String> dependencies = new ArrayList<>();
 
@@ -158,37 +201,58 @@
         for (int i = 0; i < nodes.getLength(); i++) {
             Node node = nodes.item(i);
             if (node.getNodeName().equals("name")) {
-                name = node.getTextContent();
+                name = node.getTextContent().trim();
             } else if (node.getNodeName().equals("usage")) {
-                usage = node.getTextContent();
+                usage = node.getTextContent().trim();
             } else if (node.getNodeName().equals("description")) {
-                description = node.getTextContent();
+                description = node.getTextContent().trim();
             } else if (node.getNodeName().equals("arguments")) {
                 options = parseArguments(node);
             } else if (node.getNodeName().equals("bundles")) {
-                String[] bundleNames = node.getTextContent().split(",");
-                for (String bundleName : bundleNames) {
-                    if (bundleName.trim().length() == 0) {
-                        continue;
-                    }
-                    bundles.add(bundleName);
-                }
+                bundles.addAll(parseBundles(node));
             } else if (node.getNodeName().equals("dependencies")) {
-                String[] dependencyNames = node.getTextContent().split(",");
-                for (String bundleName : dependencyNames) {
-                    if (bundleName.trim().length() == 0) {
-                        continue;
-                    }
-                    dependencies.add(bundleName);
-                }
+                dependencies.addAll(parseDependencies(node));
             }
         }
-        return new NewCommand(name, usage, description, options, bundles, dependencies);
+
+        if (name == null || usage == null || description == null) {
+            logger.warning("plugin " + pluginName + " provides an incomplete new command: " +
+                    "name='" + name + "', usage='" + usage + "', description='" + description + "', options='" + options + "'");
+            return null;
+        } else {
+            return new NewCommand(name, usage, description, options, bundles, dependencies);
+        }
+    }
+
+    private Collection<String> parseBundles(Node bundlesNode) {
+        List<String> bundles = new ArrayList<>();
+        NodeList nodes = bundlesNode.getChildNodes();
+        for (int i = 0; i < nodes.getLength(); i++) {
+            Node node = nodes.item(i);
+            if (node.getNodeName().equals("bundle")) {
+                String bundleName = node.getTextContent().trim();
+                bundles.add(bundleName);
+            }
+        }
+        return bundles;
+    }
+
+    private Collection<String> parseDependencies(Node dependenciesNode) {
+        List<String> dependencies = new ArrayList<>();
+        NodeList nodes = dependenciesNode.getChildNodes();
+        for (int i = 0; i < nodes.getLength(); i++) {
+            Node node = nodes.item(i);
+            if (node.getNodeName().equals("dependency")) {
+                String bundleName = node.getTextContent().trim();
+                dependencies.add(bundleName);
+            }
+        }
+        return dependencies;
     }
 
     private Options parseArguments(Node argumentsNode) {
-        // need to identify a way to express arguments
-        return null;
+        // TODO need to identify a way to express arguments
+        return new Options();
     }
 
 }
--- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/BasicCommandInfoTest.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/BasicCommandInfoTest.java	Fri Feb 22 16:53:10 2013 +0100
@@ -48,9 +48,9 @@
 
     @Test
     public void testBasics() {
-        final String NAME = "name";
-        final String DESCRIPTION = "description";
-        final String USAGE = "usage";
+        final String NAME = "the_name";
+        final String DESCRIPTION = "some-description";
+        final String USAGE = "some-usage";
         final Options OPTIONS = new Options();
         final List<String> RESOURCES = Collections.emptyList();
 
@@ -62,5 +62,6 @@
         assertEquals(OPTIONS, info.getOptions());
         assertEquals(RESOURCES, info.getDependencyResourceNames());
 
+        assertEquals(String.format("%s (description='%s', dependencies='%s')", NAME, DESCRIPTION, RESOURCES.toString()), info.toString());
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/CompoundCommandInfoSourceTest.java	Fri Feb 22 16:53:10 2013 +0100
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2012, 2013 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.launcher.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.commons.cli.Options;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.redhat.thermostat.common.cli.CommandInfo;
+import com.redhat.thermostat.common.cli.CommandInfoNotFoundException;
+import com.redhat.thermostat.common.cli.CommandInfoSource;
+
+public class CompoundCommandInfoSourceTest {
+
+    private CommandInfoSource source1;
+    private CommandInfoSource source2;
+    private CompoundCommandInfoSource compoundSource;
+
+    @Before
+    public void setUp() {
+        source1 = mock(CommandInfoSource.class);
+        source2 = mock(CommandInfoSource.class);
+
+        compoundSource = new CompoundCommandInfoSource(source1, source2);
+    }
+
+    @Test(expected = CommandInfoNotFoundException.class)
+    public void verifyExceptionThrownOnUnknownCommand() {
+        String NAME = "test-command-please-ignore";
+        when(source1.getCommandInfo(NAME)).thenThrow(new CommandInfoNotFoundException(NAME));
+        when(source2.getCommandInfo(NAME)).thenThrow(new CommandInfoNotFoundException(NAME));
+
+        compoundSource.getCommandInfo(NAME);
+    }
+
+    @Test
+    public void verifyGetCommandInfoDelegatesToSource1() {
+        CommandInfo cmdInfo = mock(CommandInfo.class);
+        String NAME = "test-command-please-ignore";
+        when(source1.getCommandInfo(NAME)).thenReturn(cmdInfo);
+        when(source2.getCommandInfo(NAME)).thenThrow(new CommandInfoNotFoundException(NAME));
+
+        CommandInfo result = compoundSource.getCommandInfo(NAME);
+        assertEquals(cmdInfo, result);
+    }
+
+    @Test
+    public void verifyGetCommandInfoDelegatesToSource2() {
+        CommandInfo cmdInfo = mock(CommandInfo.class);
+        String NAME = "test-command-please-ignore";
+        when(source1.getCommandInfo(NAME)).thenThrow(new CommandInfoNotFoundException(NAME));
+        when(source2.getCommandInfo(NAME)).thenReturn(cmdInfo);
+
+        CommandInfo result = compoundSource.getCommandInfo(NAME);
+        assertEquals(cmdInfo, result);
+    }
+
+    @Test
+    public void verifyGetCommandInfoMergesResultFromBothSources() {
+        String NAME = "test-command-please-ignore";
+        String DESCRIPTION = "test-description";
+        String USAGE = "test-usage";
+        Options OPTIONS = new Options();
+        List<String> DEPS1 = Arrays.asList("1test1", "1test2");
+        List<String> DEPS2 = Arrays.asList("2test1");
+
+        CommandInfo cmdInfo1 = mock(CommandInfo.class);
+        when(cmdInfo1.getName()).thenReturn(NAME);
+        when(cmdInfo1.getDescription()).thenReturn(DESCRIPTION);
+        when(cmdInfo1.getUsage()).thenReturn(USAGE);
+        when(cmdInfo1.getOptions()).thenReturn(OPTIONS);
+        when(cmdInfo1.getDependencyResourceNames()).thenReturn(DEPS1);
+
+        CommandInfo cmdInfo2 = mock(CommandInfo.class);
+        when(cmdInfo2.getName()).thenReturn(NAME);
+        when(cmdInfo2.getDependencyResourceNames()).thenReturn(DEPS2);
+
+        when(source1.getCommandInfo(NAME)).thenReturn(cmdInfo1);
+        when(source2.getCommandInfo(NAME)).thenReturn(cmdInfo2);
+
+        CommandInfo result = compoundSource.getCommandInfo(NAME);
+        assertEquals(NAME, result.getName());
+        assertEquals(DESCRIPTION, result.getDescription());
+        assertEquals(USAGE, result.getUsage());
+        assertEquals(OPTIONS, result.getOptions());
+
+        ArrayList<String> combined = new ArrayList<>(DEPS1);
+        combined.addAll(DEPS2);
+        assertEquals(combined, result.getDependencyResourceNames());
+    }
+
+    @Test
+    public void verifyGetCommandInfosMergesResultsFromBothSources() {
+        CommandInfo cmdInfo11 = mock(CommandInfo.class);
+        when(cmdInfo11.getName()).thenReturn("cmd1");
+        CommandInfo cmdInfo12 = mock(CommandInfo.class);
+        when(cmdInfo12.getName()).thenReturn("cmd2");
+
+        when(source1.getCommandInfos()).thenReturn(Arrays.asList(cmdInfo11, cmdInfo12));
+
+        CommandInfo cmdInfo21 = mock(CommandInfo.class);
+        when(cmdInfo21.getName()).thenReturn("cmd3");
+        CommandInfo cmdInfo22 = mock(CommandInfo.class);
+        when(cmdInfo22.getName()).thenReturn("cmd2");
+
+        when(source2.getCommandInfos()).thenReturn(Arrays.asList(cmdInfo21, cmdInfo22));
+
+        Collection<CommandInfo> results = compoundSource.getCommandInfos();
+
+    }
+}
--- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/LauncherImplTest.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/LauncherImplTest.java	Fri Feb 22 16:53:10 2013 +0100
@@ -39,7 +39,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.isA;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -89,7 +88,6 @@
 import com.redhat.thermostat.common.locale.LocaleResources;
 import com.redhat.thermostat.common.locale.Translate;
 import com.redhat.thermostat.common.tools.ApplicationState;
-import com.redhat.thermostat.common.utils.OSGIUtils;
 import com.redhat.thermostat.launcher.BundleManager;
 import com.redhat.thermostat.launcher.TestCommand;
 import com.redhat.thermostat.launcher.internal.HelpCommand;
@@ -162,8 +160,10 @@
         TestCommand cmd1 = new TestCommand(name1, new TestCmd1());
         CommandInfo info1 = mock(CommandInfo.class);
         when(info1.getName()).thenReturn(name1);
+        when(info1.getUsage()).thenReturn(name1 + " <--arg1 <arg>> [--arg2 <arg>]");
         Options options1 = new Options();
         Option opt1 = new Option(null, "arg1", true, null);
+        opt1.setRequired(true);
         options1.addOption(opt1);
         Option opt2 = new Option(null, "arg2", true, null);
         options1.addOption(opt2);
@@ -365,6 +365,42 @@
     }
 
     @Test
+    public void testBadOption() {
+        String expected = "Unrecognized option: --argNotAccepted\n"
+                + "usage: thermostat test1 <--arg1 <arg>> [--arg2 <arg>]\n"
+                + "                  description 1\n"
+                + "thermostat test1\n"
+                + "     --arg1 <arg>\n"
+                + "     --arg2 <arg>\n"
+                + "  -l,--logLevel <arg>\n";
+        runAndVerifyCommand(new String[] {"test1", "--arg1", "arg1value", "--argNotAccepted"}, expected, false);
+    }
+
+    @Test
+    public void testMissingRequiredOption() {
+        String expected = "Missing required option: --arg1\n"
+                + "usage: thermostat test1 <--arg1 <arg>> [--arg2 <arg>]\n"
+                + "                  description 1\n"
+                + "thermostat test1\n"
+                + "     --arg1 <arg>\n"
+                + "     --arg2 <arg>\n"
+                + "  -l,--logLevel <arg>\n";
+        runAndVerifyCommand(new String[] {"test1"}, expected, false);
+    }
+
+    @Test
+    public void testOptionMissingRequiredArgument() {
+        String expected = "Missing argument for option: arg1\n"
+                + "usage: thermostat test1 <--arg1 <arg>> [--arg2 <arg>]\n"
+                + "                  description 1\n"
+                + "thermostat test1\n"
+                + "     --arg1 <arg>\n"
+                + "     --arg2 <arg>\n"
+                + "  -l,--logLevel <arg>\n";
+        runAndVerifyCommand(new String[] {"test1", "--arg1"}, expected, false);
+    }
+
+    @Test
     public void testCommandInfoNotFound() throws CommandInfoNotFoundException, BundleException, IOException {
         when(infos.getCommandInfo("foo")).thenThrow(new CommandInfoNotFoundException("foo"));
         doThrow(new CommandInfoNotFoundException("foo")).when(registry).addBundlesFor("foo");
--- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginCommandInfoSourceTest.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginCommandInfoSourceTest.java	Fri Feb 22 16:53:10 2013 +0100
@@ -49,18 +49,22 @@
 import java.util.Arrays;
 import java.util.List;
 
+import org.apache.commons.cli.Options;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 
 import com.redhat.thermostat.common.cli.CommandInfo;
+import com.redhat.thermostat.common.cli.CommandInfoNotFoundException;
 import com.redhat.thermostat.launcher.internal.PluginConfiguration.CommandExtensions;
+import com.redhat.thermostat.launcher.internal.PluginConfiguration.NewCommand;
 
 public class PluginCommandInfoSourceTest {
 
     // name paths so anything tying to use them/create them will blow up
     private static final String PLUGIN_ROOT = "/fake/${PLUGIN_ROOT}";
     private static final String JAR_ROOT = "/fake/${JAR_ROOT}";
+
     private File jarRootDir;
     private File pluginRootDir;
     private PluginConfigurationParser parser;
@@ -92,7 +96,7 @@
         List<File> configurationFiles = configFilesCaptor.getAllValues();
         assertEquals(pluginDirs.length, configurationFiles.size());
         for (int i = 0; i < pluginDirs.length; i++) {
-            assertEquals(new File(pluginDirs[i], "plugin.conf"), configurationFiles.get(i));
+            assertEquals(new File(pluginDirs[i], "plugin.xml"), configurationFiles.get(i));
         }
     }
 
@@ -106,11 +110,18 @@
         PluginCommandInfoSource source = new PluginCommandInfoSource(jarRootDir, pluginRootDir, parser);
     }
 
+    @Test(expected = CommandInfoNotFoundException.class)
+    public void verifyMissingCommandInfo() {
+        PluginCommandInfoSource source = new PluginCommandInfoSource(jarRootDir, pluginRootDir, parser);
+
+        source.getCommandInfo("TEST");
+    }
+
     @Test
     public void verifyCommandInfoObjectsToExtendExistingCommandsAreCreated() {
         CommandExtensions extensions = mock(CommandExtensions.class);
         when(extensions.getCommandName()).thenReturn("command-name");
-        when(extensions.getAdditionalBundles()).thenReturn(Arrays.asList("additional-bundle"));
+        when(extensions.getPluginBundles()).thenReturn(Arrays.asList("additional-bundle"));
         when(extensions.getDepenedencyBundles()).thenReturn(Arrays.asList("dependency-bundle"));
 
         when(parserResult.getExtendedCommands()).thenReturn(Arrays.asList(extensions));
@@ -130,4 +141,43 @@
         assertTrue(info.getDependencyResourceNames().contains(expectedDep2Name));
     }
 
+    @Test
+    public void verifyCommandInfoObjectsForNewComamndsAreCreated() {
+        final String NAME = "command-name";
+        final String DESCRIPTION = "description of the command";
+        final String USAGE = "usage";
+        final Options OPTIONS = new Options();
+        final String PLUGIN_BUNDLE = "plugin-bundle.jar";
+        final String DEPENDENCY_BUNDLE = "dependency-bundle.jar";
+
+        NewCommand cmd = mock(NewCommand.class);
+        when(cmd.getCommandName()).thenReturn(NAME);
+        when(cmd.getDescription()).thenReturn(DESCRIPTION);
+        when(cmd.getUsage()).thenReturn(USAGE);
+        when(cmd.getOptions()).thenReturn(OPTIONS);
+        when(cmd.getPluginBundles()).thenReturn(Arrays.asList(PLUGIN_BUNDLE));
+        when(cmd.getDepenedencyBundles()).thenReturn(Arrays.asList(DEPENDENCY_BUNDLE));
+
+        when(parserResult.getNewCommands()).thenReturn(Arrays.asList(cmd));
+
+        File[] pluginDirs = new File[] { new File(PLUGIN_ROOT, "plugin1") };
+        when(pluginRootDir.listFiles()).thenReturn(pluginDirs);
+
+        PluginCommandInfoSource source = new PluginCommandInfoSource(jarRootDir, pluginRootDir, parser);
+
+        CommandInfo result = source.getCommandInfo(NAME);
+
+        assertEquals(NAME, result.getName());
+        assertEquals(DESCRIPTION, result.getDescription());
+        assertEquals(USAGE, result.getUsage());
+        assertEquals(OPTIONS, result.getOptions());
+
+        String expectedDep1Name = new File(PLUGIN_ROOT + "/plugin1/" + PLUGIN_BUNDLE).toURI().toString();
+        String expectedDep2Name = new File(JAR_ROOT + "/" + DEPENDENCY_BUNDLE).toURI().toString();
+
+        List<String> deps = result.getDependencyResourceNames();
+        assertEquals(2, deps.size());
+        assertTrue(deps.contains(expectedDep1Name));
+        assertTrue(deps.contains(expectedDep2Name));
+    }
 }
--- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParserTest.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParserTest.java	Fri Feb 22 16:53:10 2013 +0100
@@ -37,6 +37,7 @@
 package com.redhat.thermostat.launcher.internal;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import java.io.ByteArrayInputStream;
@@ -44,6 +45,7 @@
 import java.util.Arrays;
 import java.util.List;
 
+import org.apache.commons.cli.Options;
 import org.junit.Test;
 
 import com.redhat.thermostat.launcher.internal.PluginConfiguration.CommandExtensions;
@@ -55,7 +57,7 @@
     public void testEmptyConfigurationThrowsException() throws UnsupportedEncodingException {
         String config = "<?xml version=\"1.0\"?>\n";
         PluginConfigurationParser parser = new PluginConfigurationParser();
-        parser.parse(new ByteArrayInputStream(config.getBytes("UTF-8")));
+        parser.parse("test", new ByteArrayInputStream(config.getBytes("UTF-8")));
         fail("should not reach here");
     }
 
@@ -66,7 +68,7 @@
                 "<?xml version=\"1.0\"?>\n" +
                 "<plugin>\n" +
                 "</plugin>";
-        PluginConfiguration result = parser.parse(new ByteArrayInputStream(config.getBytes("UTF-8")));
+        PluginConfiguration result = parser.parse("test", new ByteArrayInputStream(config.getBytes("UTF-8")));
 
         assertEquals(0, result.getExtendedCommands().size());
         assertEquals(0, result.getNewCommands().size());
@@ -77,16 +79,22 @@
         String config = "<?xml version=\"1.0\"?>\n" +
                 "<plugin>\n" +
                 "  <commands>\n" +
-                "    <existing>\n" +
+                "    <command type='extends'>\n" +
                 "      <name>test</name>\n" +
-                "      <bundles>foo,bar,baz,</bundles>\n" +
-                "      <dependencies>thermostat-foo</dependencies>\n" +
-                "    </existing>\n" +
+                "      <bundles>\n" +
+                "        <bundle>foo</bundle>\n" +
+                "        <bundle>bar</bundle>\n" +
+                "        <bundle>baz</bundle>\n" +
+                "      </bundles>\n" +
+                "      <dependencies>\n" +
+                "        <dependency>thermostat-foo</dependency>\n" +
+                "      </dependencies>\n" +
+                "    </command>\n" +
                 "  </commands>\n" +
                 "</plugin>";
 
         PluginConfiguration result = new PluginConfigurationParser()
-                .parse(new ByteArrayInputStream(config.getBytes("UTF-8")));
+                .parse("test", new ByteArrayInputStream(config.getBytes("UTF-8")));
 
         assertEquals(0, result.getNewCommands().size());
 
@@ -95,7 +103,7 @@
 
         CommandExtensions first = extensions.get(0);
         assertEquals("test", first.getCommandName());
-        assertEquals(Arrays.asList("foo", "bar", "baz"), first.getAdditionalBundles());
+        assertEquals(Arrays.asList("foo", "bar", "baz"), first.getPluginBundles());
         assertEquals(Arrays.asList("thermostat-foo"), first.getDepenedencyBundles());
     }
 
@@ -104,18 +112,24 @@
         String config = "<?xml version=\"1.0\"?>\n" +
                 "<plugin>\n" +
                 "  <commands>\n" +
-                "    <new>\n" +
+                "    <command type='provides'>\n" +
                 "      <name>test</name>\n" +
                 "      <usage>usage: test</usage>\n" +
                 "      <description>description</description>\n" +
-                "      <bundles>foo,bar,baz,</bundles>\n" +
-                "      <dependencies>thermostat-foo</dependencies>\n" +
-                "    </new>\n" +
+                "      <bundles>\n" +
+                "        <bundle>foo</bundle>\n" +
+                "        <bundle>bar</bundle>\n" +
+                "        <bundle>baz</bundle>\n" +
+                "      </bundles>\n" +
+                "      <dependencies>\n" +
+                "        <dependency>thermostat-foo</dependency>\n" +
+                "      </dependencies>\n" +
+                "    </command>\n" +
                 "  </commands>\n" +
                 "</plugin>";
 
         PluginConfiguration result = new PluginConfigurationParser()
-                .parse(new ByteArrayInputStream(config.getBytes("UTF-8")));
+                .parse("test", new ByteArrayInputStream(config.getBytes("UTF-8")));
 
         List<CommandExtensions> extensions = result.getExtendedCommands();
         assertEquals(0, extensions.size());
@@ -127,9 +141,43 @@
         assertEquals("test", newCommand.getCommandName());
         assertEquals("usage: test", newCommand.getUsage());
         assertEquals("description", newCommand.getDescription());
-        assertEquals(null, newCommand.getOptions());
-        assertEquals(Arrays.asList("foo", "bar", "baz"), newCommand.getAdditionalBundles());
-        assertEquals(Arrays.asList("thermostat-foo"), newCommand.getCoreDepenedencyBundles());
+        Options opts = newCommand.getOptions();
+        assertTrue(opts.getOptions().isEmpty());
+        assertTrue(opts.getRequiredOptions().isEmpty());
+        assertEquals(Arrays.asList("foo", "bar", "baz"), newCommand.getPluginBundles());
+        assertEquals(Arrays.asList("thermostat-foo"), newCommand.getDepenedencyBundles());
     }
 
+    @Test
+    public void testSpacesAtStartAndEndAreTrimmed() throws UnsupportedEncodingException {
+        String config = "<?xml version=\"1.0\"?>\n" +
+                "<plugin>\n" +
+                "  <commands>\n" +
+                "    <command type='extends'>\n" +
+                "      <name>\ntest   \n</name>\n" +
+                "      <bundles>\n" +
+                "        <bundle>\n \t  \nfoo\t \n \n</bundle>\n" +
+                "        <bundle>\tbar  baz\n</bundle>\n" +
+                "        <bundle>buzz</bundle>\n" +
+                "      </bundles>\n" +
+                "      <dependencies>\n\t\n\t \t\t\n" +
+                "        <dependency>\t\t\t  thermostat-foo\n\t\t\n</dependency>\n" +
+                "      </dependencies>\n" +
+                "    </command>\n" +
+                "  </commands>\n" +
+                "</plugin>";
+
+        PluginConfiguration result = new PluginConfigurationParser()
+                .parse("test", new ByteArrayInputStream(config.getBytes("UTF-8")));
+
+        assertEquals(0, result.getNewCommands().size());
+
+        List<CommandExtensions> extensions = result.getExtendedCommands();
+        assertEquals(1, extensions.size());
+
+        CommandExtensions first = extensions.get(0);
+        assertEquals("test", first.getCommandName());
+        assertEquals(Arrays.asList("foo", "bar  baz", "buzz"), first.getPluginBundles());
+        assertEquals(Arrays.asList("thermostat-foo"), first.getDepenedencyBundles());
+    }
 }
--- a/numa/agent/src/main/java/com/redhat/thermostat/numa/agent/internal/NumaBackend.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/numa/agent/src/main/java/com/redhat/thermostat/numa/agent/internal/NumaBackend.java	Fri Feb 22 16:53:10 2013 +0100
@@ -41,9 +41,7 @@
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
-import com.redhat.thermostat.backend.Backend;
-import com.redhat.thermostat.backend.BackendID;
-import com.redhat.thermostat.backend.BackendsProperties;
+import com.redhat.thermostat.backend.BaseBackend;
 import com.redhat.thermostat.common.ApplicationService;
 import com.redhat.thermostat.common.Timer;
 import com.redhat.thermostat.common.Timer.SchedulingType;
@@ -53,7 +51,7 @@
 import com.redhat.thermostat.numa.common.NumaNodeStat;
 import com.redhat.thermostat.numa.common.NumaStat;
 
-public class NumaBackend extends Backend {
+public class NumaBackend extends BaseBackend {
 
     private static final Logger log = Logger.getLogger(NumaBackend.class.getName());
 
@@ -66,15 +64,13 @@
     private Timer timer;
 
     public NumaBackend(ApplicationService appService, NumaDAO numaDAO, NumaCollector numaCollector, Version version) {
-        super(new BackendID("NUMA Backend", NumaBackend.class.getName()));
+        super("NUMA Backend",
+                "Gathers NUMA statistics about a host",
+                "Red Hat, Inc.",
+                version.getVersionNumber());
         this.appService = appService;
         this.numaDAO = numaDAO;
         this.numaCollector = numaCollector;
-        
-        setConfigurationValue(BackendsProperties.VENDOR.name(), "Red Hat, Inc.");
-        setConfigurationValue(BackendsProperties.DESCRIPTION.name(), "Gathers NUMA statistics about a host");
-        setConfigurationValue(BackendsProperties.VERSION.name(), version.getVersionNumber());
-        
     }
 
     @Override
@@ -123,11 +119,6 @@
     }
 
     @Override
-    public boolean attachToNewProcessByDefault() {
-        return false;
-    }
-
-    @Override
     public int getOrderValue() {
         return ORDER_MEMORY_GROUP + 80;
     }
--- a/numa/agent/src/test/java/com/redhat/thermostat/numa/agent/internal/NumaBackendTest.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/numa/agent/src/test/java/com/redhat/thermostat/numa/agent/internal/NumaBackendTest.java	Fri Feb 22 16:53:10 2013 +0100
@@ -161,11 +161,6 @@
     }
 
     @Test
-    public void testAttachToNewProcessByDefault() {
-        assertFalse(backend.attachToNewProcessByDefault());
-    }
-
-    @Test
     public void testOrderValue() {
         assertEquals(Ordered.ORDER_MEMORY_GROUP + 80, backend.getOrderValue());
     }
--- a/pom.xml	Fri Feb 22 16:52:26 2013 +0100
+++ b/pom.xml	Fri Feb 22 16:53:10 2013 +0100
@@ -59,6 +59,7 @@
     <thermostat.build.directory>target</thermostat.build.directory>
     <thermostat.java.version>1.7</thermostat.java.version>
     <thermostat.home>${project.build.directory}</thermostat.home>
+    <thermostat.web.deploy.dir>${thermostat.build.directory}/${project.build.finalName}</thermostat.web.deploy.dir>
     <java.dir>/usr/share/java</java.dir>
 
     <junit.version>4.10</junit.version>
--- a/system-backend/src/main/java/com/redhat/thermostat/backend/system/SystemBackend.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/system-backend/src/main/java/com/redhat/thermostat/backend/system/SystemBackend.java	Fri Feb 22 16:53:10 2013 +0100
@@ -46,9 +46,7 @@
 import sun.jvmstat.monitor.MonitorException;
 import sun.jvmstat.monitor.MonitoredHost;
 
-import com.redhat.thermostat.backend.Backend;
-import com.redhat.thermostat.backend.BackendID;
-import com.redhat.thermostat.backend.BackendsProperties;
+import com.redhat.thermostat.backend.BaseBackend;
 import com.redhat.thermostat.common.utils.LoggingUtils;
 import com.redhat.thermostat.storage.dao.HostInfoDAO;
 import com.redhat.thermostat.storage.dao.NetworkInterfaceInfoDAO;
@@ -56,7 +54,7 @@
 import com.redhat.thermostat.storage.model.NetworkInterfaceInfo;
 import com.redhat.thermostat.utils.ProcDataSource;
 
-public class SystemBackend extends Backend {
+public class SystemBackend extends BaseBackend {
 
     private static final Logger logger = LoggingUtils.getLogger(SystemBackend.class);
 
@@ -75,14 +73,13 @@
 
 
     public SystemBackend(HostInfoDAO hostInfoDAO, NetworkInterfaceInfoDAO netInfoDAO, VmInfoDAO vmInfoDAO, VmStatusChangeNotifier notifier) {
-        super(new BackendID("System Backend", SystemBackend.class.getName()));
+        super("System Backend",
+                "Gathers basic information from the system",
+                "Red Hat, Inc.",
+                "0.5.0", true);
         this.hostInfos = hostInfoDAO;
         this.networkInterfaces = netInfoDAO;
 
-        setConfigurationValue(BackendsProperties.VENDOR.name(), "Red Hat, Inc.");
-        setConfigurationValue(BackendsProperties.DESCRIPTION.name(), "Gathers basic information from the system");
-        setConfigurationValue(BackendsProperties.VERSION.name(), "0.5.0");
-        
         ProcDataSource source = new ProcDataSource();
         hostInfoBuilder = new HostInfoBuilder(source);
         hostListener = new JvmStatHostListener(vmInfoDAO, notifier);
@@ -148,16 +145,6 @@
     }
 
     @Override
-    public String getConfigurationValue(String key) {
-        return null;
-    }
-
-    @Override
-    public boolean attachToNewProcessByDefault() {
-        return true;
-    }
-
-    @Override
     public int getOrderValue() {
         return ORDER_DEFAULT_GROUP;
     }
--- a/vm-classstat/agent/src/main/java/com/redhat/thermostat/vm/classstat/agent/internal/VmClassStatBackend.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/vm-classstat/agent/src/main/java/com/redhat/thermostat/vm/classstat/agent/internal/VmClassStatBackend.java	Fri Feb 22 16:53:10 2013 +0100
@@ -39,8 +39,6 @@
 import sun.jvmstat.monitor.event.VmListener;
 
 import com.redhat.thermostat.agent.VmStatusListenerRegistrar;
-import com.redhat.thermostat.backend.BackendID;
-import com.redhat.thermostat.backend.BackendsProperties;
 import com.redhat.thermostat.backend.VmListenerBackend;
 import com.redhat.thermostat.common.Version;
 import com.redhat.thermostat.vm.classstat.common.VmClassStatDAO;
@@ -50,22 +48,10 @@
     private final VmClassStatDAO vmClassStats;
 
     public VmClassStatBackend(VmClassStatDAO vmClassStatDAO, Version version, VmStatusListenerRegistrar registrar) {
-        super(new BackendID("VM Classes Backend", VmClassStatBackend.class.getName()), registrar);
+        super("VM Classes Backend",
+                "Gathers class loading statistics about a JVM",
+                "Red Hat, Inc.", version.getVersionNumber(), true, registrar);
         this.vmClassStats = vmClassStatDAO;
-        
-        setConfigurationValue(BackendsProperties.VENDOR.name(), "Red Hat, Inc.");
-        setConfigurationValue(BackendsProperties.DESCRIPTION.name(), "Gathers class loading statistics about a JVM");
-        setConfigurationValue(BackendsProperties.VERSION.name(), version.getVersionNumber());
-    }
-    
-    @Override
-    public String getConfigurationValue(String key) {
-        return null;
-    }
-
-    @Override
-    public boolean attachToNewProcessByDefault() {
-        return true;
     }
 
     @Override
--- a/vm-cpu/agent/src/main/java/com/redhat/thermostat/vm/cpu/agent/internal/VmCpuBackend.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/vm-cpu/agent/src/main/java/com/redhat/thermostat/vm/cpu/agent/internal/VmCpuBackend.java	Fri Feb 22 16:53:10 2013 +0100
@@ -47,9 +47,7 @@
 
 import com.redhat.thermostat.agent.VmStatusListener;
 import com.redhat.thermostat.agent.VmStatusListenerRegistrar;
-import com.redhat.thermostat.backend.Backend;
-import com.redhat.thermostat.backend.BackendID;
-import com.redhat.thermostat.backend.BackendsProperties;
+import com.redhat.thermostat.backend.BaseBackend;
 import com.redhat.thermostat.common.Clock;
 import com.redhat.thermostat.common.SystemClock;
 import com.redhat.thermostat.common.Version;
@@ -59,7 +57,7 @@
 import com.redhat.thermostat.vm.cpu.common.VmCpuStatDAO;
 import com.redhat.thermostat.vm.cpu.common.model.VmCpuStat;
 
-public class VmCpuBackend extends Backend implements VmStatusListener {
+public class VmCpuBackend extends BaseBackend implements VmStatusListener {
 
     private static final Logger LOGGER = LoggingUtils.getLogger(VmCpuBackend.class);
     static final long PROC_CHECK_INTERVAL = 1000; // TODO make this configurable.
@@ -74,15 +72,14 @@
 
     public VmCpuBackend(ScheduledExecutorService executor, VmCpuStatDAO vmCpuStatDao, Version version,
             VmStatusListenerRegistrar registrar) {
-        super(new BackendID("VM CPU Backend", VmCpuBackend.class.getName()));
+        super("VM CPU Backend",
+                "Gathers CPU statistics about a JVM",
+                "Red Hat, Inc.",
+                version.getVersionNumber(), true);
         this.executor = executor;
         this.vmCpuStats = vmCpuStatDao;
         this.registrar = registrar;
-        
-        setConfigurationValue(BackendsProperties.VENDOR.name(), "Red Hat, Inc.");
-        setConfigurationValue(BackendsProperties.DESCRIPTION.name(), "Gathers CPU statistics about a JVM");
-        setConfigurationValue(BackendsProperties.VERSION.name(), version.getVersionNumber());
-        
+
         Clock clock = new SystemClock();
         long ticksPerSecond = SysConf.getClockTicksPerSecond();
         ProcDataSource source = new ProcDataSource();
@@ -134,16 +131,6 @@
     }
 
     @Override
-    public String getConfigurationValue(String key) {
-        return null;
-    }
-
-    @Override
-    public boolean attachToNewProcessByDefault() {
-        return true;
-    }
-
-    @Override
     public int getOrderValue() {
         return ORDER_CPU_GROUP + 50;
     }
--- a/vm-gc/agent/src/main/java/com/redhat/thermostat/vm/gc/agent/internal/VmGcBackend.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/vm-gc/agent/src/main/java/com/redhat/thermostat/vm/gc/agent/internal/VmGcBackend.java	Fri Feb 22 16:53:10 2013 +0100
@@ -39,8 +39,6 @@
 import sun.jvmstat.monitor.event.VmListener;
 
 import com.redhat.thermostat.agent.VmStatusListenerRegistrar;
-import com.redhat.thermostat.backend.BackendID;
-import com.redhat.thermostat.backend.BackendsProperties;
 import com.redhat.thermostat.backend.VmListenerBackend;
 import com.redhat.thermostat.common.Version;
 import com.redhat.thermostat.vm.gc.common.VmGcStatDAO;
@@ -50,22 +48,10 @@
     private final VmGcStatDAO vmGcStats;
 
     public VmGcBackend(VmGcStatDAO vmGcStatDAO, Version version, VmStatusListenerRegistrar registrar) {
-        super(new BackendID("VM GC Backend", VmGcBackend.class.getName()), registrar);
+        super("VM GC Backend",
+                "Gathers garbage collection statistics about a JVM",
+                "Red Hat, Inc.", version.getVersionNumber(), true, registrar);
         this.vmGcStats = vmGcStatDAO;
-        
-        setConfigurationValue(BackendsProperties.VENDOR.name(), "Red Hat, Inc.");
-        setConfigurationValue(BackendsProperties.DESCRIPTION.name(), "Gathers garbage collection statistics about a JVM");
-        setConfigurationValue(BackendsProperties.VERSION.name(), version.getVersionNumber());
-    }
-    
-    @Override
-    public String getConfigurationValue(String key) {
-        return null;
-    }
-
-    @Override
-    public boolean attachToNewProcessByDefault() {
-        return true;
     }
 
     @Override
--- a/vm-memory/agent/src/main/java/com/redhat/thermostat/vm/memory/agent/internal/VmMemoryBackend.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/vm-memory/agent/src/main/java/com/redhat/thermostat/vm/memory/agent/internal/VmMemoryBackend.java	Fri Feb 22 16:53:10 2013 +0100
@@ -37,8 +37,6 @@
 package com.redhat.thermostat.vm.memory.agent.internal;
 
 import com.redhat.thermostat.agent.VmStatusListenerRegistrar;
-import com.redhat.thermostat.backend.BackendID;
-import com.redhat.thermostat.backend.BackendsProperties;
 import com.redhat.thermostat.backend.VmListenerBackend;
 import com.redhat.thermostat.common.Version;
 import com.redhat.thermostat.vm.memory.common.VmMemoryStatDAO;
@@ -48,22 +46,10 @@
     private VmMemoryStatDAO vmMemoryStats;
     
     public VmMemoryBackend(VmMemoryStatDAO vmMemoryStatDAO, Version version, VmStatusListenerRegistrar registrar) {
-        super(new BackendID("VM Memory Backend", VmMemoryBackend.class.getName()), registrar);
+        super("VM Memory Backend",
+                "Gathers memory statistics about a JVM",
+                "Red Hat, Inc.", version.getVersionNumber(), true, registrar);
         this.vmMemoryStats = vmMemoryStatDAO;
-        
-        setConfigurationValue(BackendsProperties.VENDOR.name(), "Red Hat, Inc.");
-        setConfigurationValue(BackendsProperties.DESCRIPTION.name(), "Gathers memory statistics about a JVM");
-        setConfigurationValue(BackendsProperties.VERSION.name(), version.getVersionNumber());
-    }
-    
-    @Override
-    public String getConfigurationValue(String key) {
-        return null;
-    }
-
-    @Override
-    public boolean attachToNewProcessByDefault() {
-        return true;
     }
 
     @Override
--- a/vm-overview/client-swing/src/main/java/com/redhat/thermostat/vm/overview/client/swing/internal/ChangeableText.java	Fri Feb 22 16:52:26 2013 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,82 +0,0 @@
-/*
- * Copyright 2012, 2013 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.vm.overview.client.swing.internal;
-
-import java.util.HashSet;
-import java.util.Set;
-
-public class ChangeableText {
-
-    private final Set<TextListener> listeners = new HashSet<TextListener>();
-    private String text;
-
-    public static interface TextListener {
-        public void textChanged(ChangeableText text);
-    }
-
-    public ChangeableText(String text) {
-        this.text = text;
-    }
-
-    public synchronized void setText(String text) {
-        if (this.text.equals(text)) {
-            return;
-        }
-        this.text = text;
-        fireChanged();
-    }
-
-    public synchronized String getText() {
-        return text;
-    }
-
-    public synchronized void addListener(TextListener listener) {
-        this.listeners.add(listener);
-    }
-
-    public synchronized void removeListener(TextListener listener) {
-        this.listeners.remove(listener);
-    }
-
-    private void fireChanged() {
-        for (TextListener listener: listeners) {
-            listener.textChanged(this);
-        }
-    }
-
-}
-
--- a/vm-overview/client-swing/src/main/java/com/redhat/thermostat/vm/overview/client/swing/internal/SimpleTable.java	Fri Feb 22 16:52:26 2013 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,293 +0,0 @@
-/*
- * Copyright 2012, 2013 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.vm.overview.client.swing.internal;
-
-import java.awt.Component;
-import java.awt.GridBagConstraints;
-import java.awt.GridBagLayout;
-import java.awt.Insets;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-
-import javax.swing.Box;
-import javax.swing.JComponent;
-import javax.swing.JEditorPane;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JTextArea;
-import javax.swing.JTextField;
-import javax.swing.SwingUtilities;
-
-import com.redhat.thermostat.client.swing.ComponentVisibleListener;
-import com.redhat.thermostat.client.swing.components.LabelField;
-import com.redhat.thermostat.client.swing.components.SectionHeader;
-import com.redhat.thermostat.client.swing.components.ValueField;
-
-public class SimpleTable implements ChangeableText.TextListener {
-
-    Map<ChangeableText, Set<JComponent>> updateMap = new HashMap<ChangeableText, Set<JComponent>>();
-
-    public static class Section {
-        private final String sectionName;
-        private final List<TableEntry> tableEntries = new ArrayList<TableEntry>();
-
-        public Section(String name) {
-            this.sectionName = name;
-        }
-
-        public String getText() {
-            return sectionName;
-        }
-
-        public void add(TableEntry entry) {
-            tableEntries.add(entry);
-        }
-
-        public void add(Key key, List<Value> values) {
-            tableEntries.add(new TableEntry(key, values));
-        }
-
-        public void add(Key key, Value value) {
-            tableEntries.add(new TableEntry(key, value));
-        }
-
-        public TableEntry[] getEntries() {
-            return tableEntries.toArray(new TableEntry[0]);
-        }
-    }
-
-    public static class TableEntry {
-        private final Key key;
-        private final List<Value> values;
-
-        public TableEntry(String key, ChangeableText value) {
-            this(new Key(key), new Value(value));
-        }
-
-        public TableEntry(Key key, Value value) {
-            this.key = key;
-            this.values = new ArrayList<Value>();
-            this.values.add(value);
-        }
-
-        public TableEntry(Key key, List<Value> values) {
-            this.key = key;
-            this.values = new ArrayList<Value>(values);
-        }
-
-        public Key getKey() {
-            return key;
-        }
-
-        public Value[] getValues() {
-            return values.toArray(new Value[0]);
-        }
-    }
-
-    public static class Key {
-        private final String text;
-
-        public Key(String text) {
-            this.text = text;
-        }
-
-        public String getText() {
-            return text;
-        }
-    }
-
-    public static class Value {
-        private final ChangeableText text;
-        private final Component actualComponent;
-
-        public Value(String text) {
-            this(new ChangeableText(text));
-        }
-
-        public Value(ChangeableText text) {
-            this.text = text;
-            this.actualComponent = null;
-        }
-
-        public Value(Component component) {
-            this.actualComponent = component;
-            this.text = null;
-        }
-
-        public Component getComponent() {
-            return actualComponent;
-        }
-
-        public ChangeableText getChangeableText() {
-            return text;
-        }
-    }
-
-    public JPanel createTable(List<Section> sections) {
-        final int SECTION_TOP_GAP = 10;
-        final int ROW_VERTICAL_GAP = 0;
-        final int ROW_HORIZONTAL_GAP = 10;
-
-        Insets sectionHeaderInsets = new Insets(SECTION_TOP_GAP, 0, 0, 0);
-        Insets rowInsets = new Insets(ROW_VERTICAL_GAP, ROW_HORIZONTAL_GAP, ROW_VERTICAL_GAP, ROW_HORIZONTAL_GAP);
-
-        JPanel container = new JPanel();
-        container.setLayout(new GridBagLayout());
-
-        GridBagConstraints keyConstraints = new GridBagConstraints();
-        GridBagConstraints valueConstraints = new GridBagConstraints();
-        GridBagConstraints sectionHeaderConstraints = new GridBagConstraints();
-
-        keyConstraints.insets = valueConstraints.insets = rowInsets;
-        keyConstraints.gridy = valueConstraints.gridy = 0;
-        keyConstraints.gridx = 0;
-        keyConstraints.anchor = GridBagConstraints.FIRST_LINE_END;
-        valueConstraints.gridx = 1;
-        keyConstraints.fill = valueConstraints.fill = GridBagConstraints.HORIZONTAL;
-
-        sectionHeaderConstraints.gridx = 0;
-        sectionHeaderConstraints.gridwidth = GridBagConstraints.REMAINDER;
-        sectionHeaderConstraints.fill = GridBagConstraints.HORIZONTAL;
-        sectionHeaderConstraints.insets = sectionHeaderInsets;
-
-        for (Section section : sections) {
-            sectionHeaderConstraints.gridy = keyConstraints.gridy = ++valueConstraints.gridy;
-            container.add(new SectionHeader(section.getText()), sectionHeaderConstraints);
-            for (TableEntry tableEntry : section.getEntries()) {
-                keyConstraints.gridy = ++valueConstraints.gridy;
-                container.add(new LabelField(tableEntry.getKey().getText()), keyConstraints);
-
-                for (Value value : tableEntry.getValues()) {
-                    if (value.getComponent() == null) {
-                        ChangeableText text = value.getChangeableText();
-                        JComponent valueLabel = new ValueField(text.getText());
-                        if (updateMap.containsKey(text)) {
-                            updateMap.get(text).add(valueLabel);
-                        } else {
-                            Set<JComponent> set = new HashSet<JComponent>();
-                            set.add(valueLabel);
-                            updateMap.put(text, set);
-                        }
-                        container.add(valueLabel, valueConstraints);
-                    } else {
-                        container.add(value.getComponent(), valueConstraints);
-                    }
-                    keyConstraints.gridy = ++valueConstraints.gridy;
-                }
-            }
-        }
-
-        GridBagConstraints glueConstraints = new GridBagConstraints();
-        glueConstraints.gridy = keyConstraints.gridy + 1;
-        glueConstraints.gridx = 0;
-        glueConstraints.weightx = 1;
-        glueConstraints.weighty = 1;
-        glueConstraints.fill = GridBagConstraints.BOTH;
-        glueConstraints.gridheight = GridBagConstraints.REMAINDER;
-        glueConstraints.gridwidth = GridBagConstraints.REMAINDER;
-        Component filler = Box.createGlue();
-        container.add(filler, glueConstraints);
-
-        container.addHierarchyListener(new ComponentVisibleListener() {
-            @Override
-            public void componentShown(Component c) {
-                updateAllValues();
-                addAllListeners();
-            }
-
-            @Override
-            public void componentHidden(Component c) {
-                removeAllListeners();
-            }
-        });
-
-        return container;
-    }
-
-
-    private void updateAllValues() {
-        for (Entry<ChangeableText, Set<JComponent>> entry: updateMap.entrySet()) {
-            for (JComponent label: entry.getValue()) {
-                setText(label, entry.getKey().getText());
-            }
-        }
-    }
-
-    private static void setText(JComponent target, String text) {
-        if (target instanceof JLabel) {
-            ((JLabel)target).setText(text);
-        } else if (target instanceof JTextField) {
-            ((JTextField)target).setText(text);
-        } else if (target instanceof JTextArea) {
-            ((JTextArea)target).setText(text);
-        } else if (target instanceof JEditorPane) {
-            ((JEditorPane)target).setText(text);
-        }
-    }
-
-    @Override
-    public void textChanged(final ChangeableText text) {
-        SwingUtilities.invokeLater(new Runnable() {
-            @Override
-            public void run() {
-                String newValue = text.getText();
-                for (JComponent label: updateMap.get(text)) {
-                    setText(label, newValue);
-                }
-            }
-        });
-    }
-
-    public void addAllListeners() {
-        for (ChangeableText text : updateMap.keySet()) {
-            text.addListener(this);
-        }
-    }
-
-    public void removeAllListeners() {
-        for (ChangeableText text : updateMap.keySet()) {
-            text.removeListener(this);
-        }
-    }
-
-}
-
--- a/vm-overview/client-swing/src/main/java/com/redhat/thermostat/vm/overview/client/swing/internal/VmOverviewPanel.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/vm-overview/client-swing/src/main/java/com/redhat/thermostat/vm/overview/client/swing/internal/VmOverviewPanel.java	Fri Feb 22 16:53:10 2013 +0100
@@ -37,40 +37,42 @@
 package com.redhat.thermostat.vm.overview.client.swing.internal;
 
 import java.awt.Component;
-import java.util.ArrayList;
-import java.util.List;
 
 import javax.swing.BorderFactory;
+import javax.swing.GroupLayout;
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
+import javax.swing.SwingUtilities;
+import javax.swing.GroupLayout.Alignment;
+import javax.swing.LayoutStyle.ComponentPlacement;
 
 import com.redhat.thermostat.client.swing.ComponentVisibleListener;
 import com.redhat.thermostat.client.swing.SwingComponent;
 import com.redhat.thermostat.client.swing.components.HeaderPanel;
+import com.redhat.thermostat.client.swing.components.LabelField;
+import com.redhat.thermostat.client.swing.components.SectionHeader;
+import com.redhat.thermostat.client.swing.components.ValueField;
 import com.redhat.thermostat.common.ActionListener;
 import com.redhat.thermostat.common.locale.Translate;
 import com.redhat.thermostat.vm.overview.client.core.VmOverviewView;
 import com.redhat.thermostat.vm.overview.client.locale.LocaleResources;
-import com.redhat.thermostat.vm.overview.client.swing.internal.SimpleTable.Section;
-import com.redhat.thermostat.vm.overview.client.swing.internal.SimpleTable.TableEntry;
 
 public class VmOverviewPanel extends VmOverviewView implements SwingComponent {
 
     private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
-    
+
     private HeaderPanel visiblePanel;
     private JScrollPane container;
 
-    private final ChangeableText pid = new ChangeableText("");
-    private final ChangeableText startTimeStamp = new ChangeableText("");
-    private final ChangeableText stopTimeStamp = new ChangeableText("");
-    private final ChangeableText mainClass = new ChangeableText("");
-    private final ChangeableText javaCommandLine = new ChangeableText("");
-    private final ChangeableText javaHome = new ChangeableText("");
-    private final ChangeableText javaVersion = new ChangeableText("");
-    private final ChangeableText vmNameAndVersion = new ChangeableText("");
-    private final ChangeableText vmArguments = new ChangeableText("");
-
+    private final ValueField pid = new ValueField("");
+    private final ValueField startTimeStamp = new ValueField("");
+    private final ValueField stopTimeStamp = new ValueField("");
+    private final ValueField mainClass = new ValueField("");
+    private final ValueField javaCommandLine = new ValueField("");
+    private final ValueField javaHome = new ValueField("");
+    private final ValueField javaVersion = new ValueField("");
+    private final ValueField vmNameAndVersion = new ValueField("");
+    private final ValueField vmArguments = new ValueField("");
 
     public VmOverviewPanel() {
         super();
@@ -99,49 +101,94 @@
     }
 
     @Override
-    public void setVmPid(String pid) {
-        this.pid.setText(pid);
+    public void setVmPid(final String newPid) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                pid.setText(newPid);
+            }
+        });
     }
 
     @Override
-    public void setVmStartTimeStamp(String timeStamp) {
-        this.startTimeStamp.setText(timeStamp);
+    public void setVmStartTimeStamp(final String newTimeStamp) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                startTimeStamp.setText(newTimeStamp);
+            }
+        });
     }
 
     @Override
-    public void setVmStopTimeStamp(String timeStamp) {
-        this.stopTimeStamp.setText(timeStamp);
+    public void setVmStopTimeStamp(final String newTimeStamp) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                stopTimeStamp.setText(newTimeStamp);
+            }
+        });
     }
 
     @Override
-    public void setMainClass(String mainClass) {
-        this.mainClass.setText(mainClass);
+    public void setMainClass(final String newMainClass) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                mainClass.setText(newMainClass);
+            }
+        });
     }
 
     @Override
-    public void setJavaCommandLine(String javaCommandLine) {
-        this.javaCommandLine.setText(javaCommandLine);
+    public void setJavaCommandLine(final String newJavaCommandLine) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                javaCommandLine.setText(newJavaCommandLine);
+            }
+        });
     }
 
     @Override
-    public void setJavaHome(String javaHome) {
-        this.javaHome.setText(javaHome);
+    public void setJavaHome(final String newJavaHome) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                javaHome.setText(newJavaHome);
+            }
+        });
 
     }
 
     @Override
-    public void setJavaVersion(String javaVersion) {
-        this.javaVersion.setText(javaVersion);
+    public void setJavaVersion(final String newJavaVersion) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                javaVersion.setText(newJavaVersion);
+            }
+        });
     }
 
     @Override
-    public void setVmNameAndVersion(String vmNameAndVersion) {
-        this.vmNameAndVersion.setText(vmNameAndVersion);
+    public void setVmNameAndVersion(final String newVmNameAndVersion) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                vmNameAndVersion.setText(newVmNameAndVersion);
+            }
+        });
     }
 
     @Override
-    public void setVmArguments(String vmArguments) {
-        this.vmArguments.setText(vmArguments);
+    public void setVmArguments(final String newVmArguments) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                vmArguments.setText(newVmArguments);
+            }
+        });
     }
 
     @Override
@@ -154,40 +201,92 @@
 
         visiblePanel.setHeader(translator.localize(LocaleResources.VM_INFO_TITLE));
 
-        TableEntry entry;
-        List<Section> allSections = new ArrayList<Section>();
+        SectionHeader processSection = new SectionHeader(translator.localize(LocaleResources.VM_INFO_SECTION_PROCESS));
+        LabelField pidLabel = new LabelField(translator.localize(LocaleResources.VM_INFO_PROCESS_ID));
+        LabelField startTimeLabel = new LabelField(translator.localize(LocaleResources.VM_INFO_START_TIME));
+        LabelField stopTimeLabel = new LabelField(translator.localize(LocaleResources.VM_INFO_STOP_TIME));
 
-        Section processSection = new Section(translator.localize(LocaleResources.VM_INFO_SECTION_PROCESS));
-        allSections.add(processSection);
+        SectionHeader javaSection = new SectionHeader(translator.localize(LocaleResources.VM_INFO_SECTION_JAVA));
 
-        entry = new TableEntry(translator.localize(LocaleResources.VM_INFO_PROCESS_ID), pid);
-        processSection.add(entry);
-        entry = new TableEntry(translator.localize(LocaleResources.VM_INFO_START_TIME), startTimeStamp);
-        processSection.add(entry);
-        entry = new TableEntry(translator.localize(LocaleResources.VM_INFO_STOP_TIME), stopTimeStamp);
-        processSection.add(entry);
+        LabelField mainClassLabel = new LabelField(translator.localize(LocaleResources.VM_INFO_MAIN_CLASS));
+        LabelField javaCommandLineLabel = new LabelField(translator.localize(LocaleResources.VM_INFO_COMMAND_LINE));
+        LabelField javaVersionLabel = new LabelField(translator.localize(LocaleResources.VM_INFO_JAVA_VERSION));
+        LabelField vmNameAndVersionLabel = new LabelField(translator.localize(LocaleResources.VM_INFO_VM));
+        LabelField vmArgumentsLabel = new LabelField(translator.localize(LocaleResources.VM_INFO_VM_ARGUMENTS));
+
+        JPanel table = new JPanel();
+        table.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 15));
+        GroupLayout gl = new GroupLayout(table);
+        table.setLayout(gl);
 
-        Section javaSection = new Section(translator.localize(LocaleResources.VM_INFO_SECTION_JAVA));
-        allSections.add(javaSection);
+        gl.setHorizontalGroup(gl.createParallelGroup()
+                .addComponent(processSection)
+                .addComponent(javaSection)
+                .addGroup(gl.createSequentialGroup()
+                        .addContainerGap()
+                        .addGroup(gl.createParallelGroup(Alignment.TRAILING)
+                                .addComponent(pidLabel)
+                                .addComponent(startTimeLabel)
+                                .addComponent(stopTimeLabel)
+                                .addComponent(mainClassLabel)
+                                .addComponent(javaCommandLineLabel)
+                                .addComponent(javaVersionLabel)
+                                .addComponent(vmNameAndVersionLabel)
+                                .addComponent(vmArgumentsLabel))
+                        .addPreferredGap(ComponentPlacement.RELATED)
+                        .addGroup(gl.createParallelGroup()
+                                .addComponent(pid)
+                                .addComponent(startTimeStamp)
+                                .addComponent(stopTimeStamp)
+                                .addComponent(mainClass)
+                                .addComponent(javaCommandLine)
+                                .addComponent(javaVersion)
+                                .addComponent(vmNameAndVersion)
+                                .addComponent(vmArguments))
+                        .addContainerGap()));
 
-        entry = new TableEntry(translator.localize(LocaleResources.VM_INFO_MAIN_CLASS), mainClass);
-        javaSection.add(entry);
-        entry = new TableEntry(translator.localize(LocaleResources.VM_INFO_COMMAND_LINE), javaCommandLine);
-        javaSection.add(entry);
-        entry = new TableEntry(translator.localize(LocaleResources.VM_INFO_JAVA_VERSION), javaVersion);
-        javaSection.add(entry);
-        entry = new TableEntry(translator.localize(LocaleResources.VM_INFO_VM), vmNameAndVersion);
-        javaSection.add(entry);
-        entry = new TableEntry(translator.localize(LocaleResources.VM_INFO_VM_ARGUMENTS), vmArguments);
-        javaSection.add(entry);
-
-        SimpleTable simpleTable = new SimpleTable();
-        JPanel table = simpleTable.createTable(allSections);
-        table.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+        gl.setVerticalGroup(gl.createSequentialGroup()
+                .addContainerGap()
+                .addComponent(processSection)
+                .addPreferredGap(ComponentPlacement.RELATED)
+                .addGroup(gl.createParallelGroup(Alignment.LEADING, false)
+                        .addComponent(pidLabel)
+                        .addComponent(pid))
+                .addPreferredGap(ComponentPlacement.RELATED)
+                .addGroup(gl.createParallelGroup(Alignment.LEADING, false)
+                        .addComponent(startTimeLabel)
+                        .addComponent(startTimeStamp))
+                .addPreferredGap(ComponentPlacement.RELATED)
+                .addGroup(gl.createParallelGroup(Alignment.LEADING, false)
+                        .addComponent(stopTimeLabel)
+                        .addComponent(stopTimeStamp))
+                .addPreferredGap(ComponentPlacement.UNRELATED)
+                .addComponent(javaSection)
+                .addPreferredGap(ComponentPlacement.RELATED)
+                .addGroup(gl.createParallelGroup(Alignment.LEADING, false)
+                        .addComponent(mainClassLabel)
+                        .addComponent(mainClass))
+                .addPreferredGap(ComponentPlacement.RELATED)
+                .addGroup(gl.createParallelGroup(Alignment.LEADING, false)
+                        .addComponent(javaCommandLineLabel)
+                        .addComponent(javaCommandLine))
+                .addPreferredGap(ComponentPlacement.RELATED)
+                .addGroup(gl.createParallelGroup(Alignment.LEADING, false)
+                        .addComponent(javaVersionLabel)
+                        .addComponent(javaVersion))
+                .addPreferredGap(ComponentPlacement.RELATED)
+                .addGroup(gl.createParallelGroup(Alignment.LEADING, false)
+                        .addComponent(vmNameAndVersionLabel)
+                        .addComponent(vmNameAndVersion))
+                .addPreferredGap(ComponentPlacement.RELATED)
+                .addGroup(gl.createParallelGroup(Alignment.LEADING, false)
+                        .addComponent(vmArgumentsLabel)
+                        .addComponent(vmArguments))
+                .addGap(0, 0, Short.MAX_VALUE)
+                .addContainerGap());
 
         container = new JScrollPane(table, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
 
         visiblePanel.setContent(container);
     }
 }
-
--- a/web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java	Fri Feb 22 16:53:10 2013 +0100
@@ -36,6 +36,7 @@
 
 package com.redhat.thermostat.web.server;
 
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -113,15 +114,20 @@
     @Override
     public void init(ServletConfig config) throws ServletException {
         super.init(config);
+        logger.log(Level.INFO, "Initializing web service");
         if (!isThermostatHomeSet()) {
             // This is the webapp and our entry point into thermostat's web
             // service. The launcher did not run and hence THERMOSTAT_HOME is
             // not set and we need to do this ourselves.
-            // In this case THERMOSTAT_HOME is in the WEB-INF/thermostat folder
-            // in order to make it inaccessible via HTTP. This is not a "real"
-            // THERMOSTAT_HOME. For now it only contains an ssl.properties file.
-            String thermostatHome = config.getServletContext().getRealPath(
-                    "/WEB-INF/thermostat");
+            String thermostatHome = config.getInitParameter("THERMOSTAT_HOME");
+            File thermostatHomeFile = new File(thermostatHome);
+            if (!thermostatHomeFile.canRead()) {
+                // This is bad news. If we can't at least read THERMOSTAT_HOME
+                // we are bound to fail in some weird ways at some later point.
+                throw new RuntimeException("THERMOSTAT_HOME = "
+                        + thermostatHome
+                        + " is not readable or does not exist!");
+            }
             logger.log(Level.INFO, "Setting THERMOSTAT_HOME for webapp to "
                     + thermostatHome);
             System.setProperty("THERMOSTAT_HOME", thermostatHome);
--- a/web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java	Fri Feb 22 16:52:26 2013 +0100
+++ b/web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java	Fri Feb 22 16:53:10 2013 +0100
@@ -153,6 +153,10 @@
     @Before
     public void setUp() throws Exception {
 
+        // Set thermostat home to something so we don't set
+        // it in WebStorageEndPoint.init()
+        System.setProperty("THERMOSTAT_HOME", "does not matter");
+        
         mockStorage = mock(Storage.class);
         StorageWrapper.setStorage(mockStorage);
 
--- a/web/war/pom.xml	Fri Feb 22 16:52:26 2013 +0100
+++ b/web/war/pom.xml	Fri Feb 22 16:53:10 2013 +0100
@@ -88,28 +88,59 @@
           <skip>true</skip>
         </configuration>
       </plugin>
+
+      <!--
+	   Package the web archive (WAR) as an exploded WAR *and WAR. Downstream
+           distributions can build and deploy the war via a maven build call similar to
+           the following (from the top level dir):
+           $ mvn -Dthermostat.web.deploy.dir=<path-to-tomcat>/webapps/thermostat clean package
+       -->
       <plugin>
-        <artifactId>maven-resources-plugin</artifactId>
-        <version>2.5</version>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-war-plugin</artifactId>
+        <version>2.3</version>
         <executions>
           <execution>
-            <id>copy-logging-properties</id>
-            <phase>prepare-package</phase>
+            <id>exploded-war</id>
+            <phase>package</phase>
             <goals>
-              <goal>copy-resources</goal>
+              <goal>exploded</goal>
             </goals>
             <configuration>
-              <outputDirectory>src/main/webapp/WEB-INF/thermostat</outputDirectory>
-              <resources>
+              <!-- web.xml contains properties, which we'd like to have interpolated -->
+              <webResources>
                 <resource>
-                  <directory>src/../../../distribution/config</directory>
-                  <targetPath>etc</targetPath>
                   <filtering>true</filtering>
+                  <directory>src/main/webapp</directory>
                   <includes>
-                    <include>ssl.properties</include>
+                    <include>**/web.xml</include>
                   </includes>
                 </resource>
-              </resources>
+              </webResources>
+              <warSourceDirectory>src/main/webapp</warSourceDirectory>
+              <webXml>src/main/webapp/WEB-INF/web.xml</webXml>
+              <webappDirectory>${thermostat.web.deploy.dir}</webappDirectory>
+            </configuration>
+          </execution>
+          <execution>
+            <id>war</id>
+            <phase>package</phase>
+            <goals>
+              <goal>war</goal>
+            </goals>
+            <configuration>
+              <!-- web.xml contains properties, which we'd like to have interpolated -->
+              <webResources>
+                <resource>
+                  <filtering>true</filtering>
+                  <directory>src/main/webapp</directory>
+                  <includes>
+                    <include>**/web.xml</include>
+                  </includes>
+                </resource>
+              </webResources>
+              <warSourceDirectory>src/main/webapp</warSourceDirectory>
+              <webXml>src/main/webapp/WEB-INF/web.xml</webXml>
             </configuration>
           </execution>
         </executions>
--- a/web/war/src/main/webapp/WEB-INF/web.xml	Fri Feb 22 16:52:26 2013 +0100
+++ b/web/war/src/main/webapp/WEB-INF/web.xml	Fri Feb 22 16:53:10 2013 +0100
@@ -12,6 +12,11 @@
       <param-value>com.redhat.thermostat.storage.mongodb.MongoStorageProvider</param-value>
     </init-param>
     <init-param>
+      <!-- Absolute path to THERMOSTAT_HOME -->
+      <param-name>THERMOSTAT_HOME</param-name>
+      <param-value>${project.build.directory}/../../../distribution/target/</param-value>
+    </init-param>
+    <init-param>
       <param-name>storage.endpoint</param-name>
       <param-value>mongodb://127.0.0.1:27518</param-value>
     </init-param>