changeset 982:3723566e9c23

Allow plugins to contribute commands Reviewed-by: neugens Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-February/005672.html
author Omair Majid <omajid@redhat.com>
date Tue, 19 Feb 2013 12:08:47 -0500
parents 7cb166add707
children cb50a6f7f0d3
files launcher/src/main/java/com/redhat/thermostat/launcher/internal/BasicCommandInfo.java launcher/src/main/java/com/redhat/thermostat/launcher/internal/CompoundCommandInfoSource.java launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginCommandInfoSource.java launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfiguration.java launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParser.java launcher/src/test/java/com/redhat/thermostat/launcher/internal/BasicCommandInfoTest.java launcher/src/test/java/com/redhat/thermostat/launcher/internal/CompoundCommandInfoSourceTest.java launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginCommandInfoSourceTest.java launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParserTest.java
diffstat 9 files changed, 416 insertions(+), 88 deletions(-) [+]
line wrap: on
line diff
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/BasicCommandInfo.java	Tue Feb 19 16:22:03 2013 +0100
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/BasicCommandInfo.java	Tue Feb 19 12:08:47 2013 -0500
@@ -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	Tue Feb 19 16:22:03 2013 +0100
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/CompoundCommandInfoSource.java	Tue Feb 19 12:08:47 2013 -0500
@@ -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/PluginCommandInfoSource.java	Tue Feb 19 16:22:03 2013 +0100
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginCommandInfoSource.java	Tue Feb 19 12:08:47 2013 -0500
@@ -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,12 +54,16 @@
 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;
 
 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());
@@ -74,28 +79,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 +112,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	Tue Feb 19 16:22:03 2013 +0100
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfiguration.java	Tue Feb 19 12:08:47 2013 -0500
@@ -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	Tue Feb 19 16:22:03 2013 +0100
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParser.java	Tue Feb 19 12:08:47 2013 -0500
@@ -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,27 @@
 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}.
+ * <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 +87,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 +103,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 +111,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<>();
@@ -126,31 +146,23 @@
             if (node.getNodeName().equals("name")) {
                 name = node.getTextContent();
             } 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<>();
 
@@ -166,29 +178,50 @@
             } 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();
+                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();
+                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	Tue Feb 19 16:22:03 2013 +0100
+++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/BasicCommandInfoTest.java	Tue Feb 19 12:08:47 2013 -0500
@@ -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	Tue Feb 19 12:08:47 2013 -0500
@@ -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/PluginCommandInfoSourceTest.java	Tue Feb 19 16:22:03 2013 +0100
+++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginCommandInfoSourceTest.java	Tue Feb 19 12:08:47 2013 -0500
@@ -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	Tue Feb 19 16:22:03 2013 +0100
+++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParserTest.java	Tue Feb 19 12:08:47 2013 -0500
@@ -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,11 @@
         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());
     }
 
 }