changeset 1076:0e9e22f2696a

Merge
author Elliott Baron <ebaron@redhat.com>
date Mon, 29 Apr 2013 11:54:04 -0400
parents be3470750169 (current diff) dead5cab5a9a (diff)
children 00c6bf165dce
files
diffstat 15 files changed, 492 insertions(+), 167 deletions(-) [+]
line wrap: on
line diff
--- a/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/timeline/TimelineRulerHeader.java	Thu Apr 25 11:28:32 2013 -0400
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/timeline/TimelineRulerHeader.java	Mon Apr 29 11:54:04 2013 -0400
@@ -56,11 +56,31 @@
 @SuppressWarnings("serial")
 public abstract class TimelineRulerHeader extends GradientPanel {
 
+    /** Default height of this component. Subclasses may use different values */
+    public static final int DEFAULT_HEIGHT = 25;
+    
+    /**
+     * Default increment is 20 pixels per units.
+     * Subclasses may use different values.
+     * 
+     * @see #DEFAULT_INCREMENT_IN_MILLIS
+     */
+    public static final int DEFAULT_INCREMENT_IN_PIXELS = 20;
+
+    /**
+     * Default increments is 1 second (1000 ms) per pixel unit.
+     * Subclasses may use different values.
+     * 
+     * @see #DEFAULT_INCREMENT_IN_PIXELS
+     */
+    public static final long DEFAULT_INCREMENT_IN_MILLIS = 1_000;
+    
     private LongRange range;
     
     public TimelineRulerHeader(LongRange range) {
         
         super(Palette.LIGHT_GRAY.getColor(), Palette.WHITE.getColor());
+        setFont(TimelineUtils.FONT);
         
         this.range = range;
     }
@@ -71,7 +91,7 @@
     
     @Override
     public int getHeight() {
-        return 25;
+        return DEFAULT_HEIGHT;
     }
     
     @Override
@@ -87,6 +107,20 @@
         return getPreferredSize();
     }
     
+    /**
+     * Defines the distance, in pixels, between one tick mark and the other.
+     */
+    public int getUnitIncrementInPixels() {
+        return DEFAULT_INCREMENT_IN_PIXELS;
+    }
+    
+    /**
+     * Defines how many milliseconds pass between two tick marks.
+     */
+    public long getUnitIncrementInMillis() {
+        return DEFAULT_INCREMENT_IN_MILLIS;
+    }
+    
     protected abstract int getCurrentDisplayValue();
     
     @Override
@@ -99,11 +133,11 @@
         int currentValue = getCurrentDisplayValue();
 
         Rectangle bounds = g.getClipBounds();
-        int totalInc = TimelineUtils.drawMarks(range, graphics, bounds, currentValue,
-                                               TimelineUtils.calculateWidth(range),
-                                               getHeight(), true);
+        
+        int unitIncrement = getUnitIncrementInPixels();
         
-        drawTimelineStrings(graphics, currentValue, bounds, totalInc);
+        TimelineUtils.drawMarks(range, graphics, bounds, currentValue, false, unitIncrement);
+        drawTimelineStrings(graphics, currentValue, bounds, unitIncrement);
         
         graphics.setColor(Palette.THERMOSTAT_BLU.getColor());
         graphics.drawLine(bounds.x, bounds.height - 1, bounds.width, bounds.height - 1);
@@ -113,18 +147,19 @@
     
     private void drawTimelineStrings(Graphics2D graphics, int currentValue, Rectangle bounds, int totalInc) {
         
-        Font font = TimelineUtils.FONT;
-        
-        graphics.setFont(font);
-        
+        Font font = graphics.getFont();
+                
         DateFormat df = new SimpleDateFormat("HH:mm:ss");
         
         Paint gradient = new GradientPaint(0, 0, Palette.WHITE.getColor(), 0, getHeight(), Palette.GRAY.getColor());
         
         graphics.setColor(Palette.EARL_GRAY.getColor());
+
+        long incrementInMillis = getUnitIncrementInMillis();
         
-        long round = range.getMin() % (TimelineUtils.STEP * 10);
-        int shift = (int) (round / TimelineUtils.STEP) * totalInc;
+        long round = range.getMin() % (10 * incrementInMillis);
+        
+        int shift = (int) (round / incrementInMillis) * totalInc;
         long currentTime = range.getMin() - round;
         
         int lowerBound = bounds.x - (4 * totalInc);
@@ -148,7 +183,7 @@
                 graphics.setColor(Palette.THERMOSTAT_BLU.getColor());                
                 graphics.drawString(value, i + 1, bounds.y + stringHeight + 5);
             }
-            currentTime += TimelineUtils.STEP;
+            currentTime += incrementInMillis;
             increment++;
         }
     }
--- a/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/timeline/TimelineUtils.java	Thu Apr 25 11:28:32 2013 -0400
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/timeline/TimelineUtils.java	Mon Apr 29 11:54:04 2013 -0400
@@ -42,46 +42,28 @@
 
 import com.redhat.thermostat.client.ui.Palette;
 import com.redhat.thermostat.common.model.LongRange;
-import com.redhat.thermostat.common.model.LongRangeNormalizer;
 
 public class TimelineUtils {
-    public static final int INC = 50;
-    public static final int STEP = 1000;
     public static final Font FONT = new Font("SansSerif", Font.PLAIN, 10);
-
-    public static int calculateWidth(LongRange range) {
-        long span = range.getMax() - range.getMin();
-        int width = (int) (span / TimelineUtils.INC);
-        return width;
-    }
-    
-    public static int drawMarks(LongRange range, Graphics2D graphics, Rectangle bounds, int currentValue, int width, int height) {
-        return drawMarks(range, graphics, bounds, currentValue, width, height, false);
-    }
-    
-    public static int drawMarks(LongRange range, Graphics2D graphics, Rectangle bounds,
-                                int currentValue, int width, int height, boolean darkerTop)
+ 
+    public static void drawMarks(LongRange range, Graphics2D graphics, Rectangle bounds,
+                                 int currentValue, boolean darkerTop, int increment)
     {
-        LongRangeNormalizer normalizer = new LongRangeNormalizer(range, 0, width);
-        normalizer.setValue(range.getMin() + TimelineUtils.STEP);
-        int totalInc = (int) normalizer.getValueNormalized();
-
-        int inc = currentValue % totalInc;
+        int inc = currentValue % increment;
         int x = (bounds.x - inc);
         
         graphics.setColor(Palette.GRAY.getColor());
         int upperBound = (bounds.x + bounds.width);
 
-        for (int i = x; i < upperBound; i += totalInc) {
-            graphics.drawLine(i, 0, i, height);
+        for (int i = x; i < upperBound; i += increment) {
+            graphics.drawLine(i, 0, i, bounds.height);
             if (darkerTop) {
                 graphics.setColor(Palette.DARK_GRAY.getColor());
                 graphics.drawLine(i, 0, i, 5);
                 graphics.setColor(Palette.GRAY.getColor());
             }
         }
-        
-        return totalInc;
     }
+
 }
 
--- a/distribution/docs/plugin.xsd	Thu Apr 25 11:28:32 2013 -0400
+++ b/distribution/docs/plugin.xsd	Mon Apr 29 11:54:04 2013 -0400
@@ -9,6 +9,7 @@
 <xs:element name="name" type="xs:string"/>
 <xs:element name="bundle" type="xs:string"/>
 <xs:element name="dependency" type="xs:string"/>
+<xs:element name="usage" type="xs:string"/>
 <xs:element name="description" type="xs:string"/>
 <xs:element name="short" type="xs:string"/>
 <xs:element name="long" type="xs:string"/>
@@ -62,7 +63,9 @@
   <xs:complexType>
     <xs:sequence>
       <xs:element ref="name"/>
+      <xs:element ref="usage" minOccurs="0" maxOccurs="1"/>
       <xs:element ref="description"/>
+      <xs:element ref="arguments" minOccurs="0" maxOccurs="1"/>
       <xs:element ref="options"/>
       <xs:element ref="bundles"/>
       <xs:element ref="dependencies"/>
@@ -71,6 +74,14 @@
 </xs:element> 
 
 
+<xs:element name="arguments">
+  <xs:complexType>
+    <xs:sequence>
+      <xs:element ref="argument" minOccurs="0" maxOccurs="1"/>
+    </xs:sequence>
+  </xs:complexType>
+</xs:element>
+
 <xs:element name="options">
   <xs:complexType>
     <xs:sequence>
--- a/integration-tests/src/test/java/com/redhat/thermostat/itest/StorageConnectionTest.java	Thu Apr 25 11:28:32 2013 -0400
+++ b/integration-tests/src/test/java/com/redhat/thermostat/itest/StorageConnectionTest.java	Mon Apr 29 11:54:04 2013 -0400
@@ -40,6 +40,7 @@
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
+import org.junit.Ignore;
 import org.junit.Test;
 
 import expectj.ExpectJException;
@@ -66,6 +67,7 @@
         assertNoExceptions(storage.getCurrentStandardOutContents(), storage.getCurrentStandardErrContents());
     }
 
+    @Ignore //FIXME when keyring/preferences improvements have been made, un-Ignore
     @Test
     public void testConnect() throws ExpectJException, TimeoutException, IOException {
         Spawn shell = spawnThermostat(true, "shell");
@@ -95,6 +97,7 @@
         assertNoExceptions(shell.getCurrentStandardOutContents(), shell.getCurrentStandardErrContents());
     }
 
+    @Ignore //FIXME when keyring/preferences improvements have been made, un-Ignore
     @Test
     public void testConnectAndDisconnectInShell() throws IOException, TimeoutException, ExpectJException {
         Spawn shell = spawnThermostat(true, "shell");
--- a/integration-tests/src/test/java/com/redhat/thermostat/itest/VmCommandsTest.java	Thu Apr 25 11:28:32 2013 -0400
+++ b/integration-tests/src/test/java/com/redhat/thermostat/itest/VmCommandsTest.java	Mon Apr 29 11:54:04 2013 -0400
@@ -45,6 +45,7 @@
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
+import org.junit.Ignore;
 import org.junit.Test;
 
 import expectj.Spawn;
@@ -69,6 +70,7 @@
         stopStorage.waitFor();
     }
 
+    @Ignore //FIXME when keyring/preferences improvements have been made, un-Ignore
     @Test
     public void testListVms() throws Exception {
         Spawn vmList = commandAgainstMongo("list-vms");
@@ -99,6 +101,7 @@
         assertNoExceptions(vmInfo.getCurrentStandardOutContents(), vmInfo.getCurrentStandardErrContents());
     }
 
+    @Ignore //FIXME when keyring/preferences improvements have been made, un-Ignore
     @Test
     public void testHeapCommands() throws Exception {
         String[] commands = new String[] {
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginCommandInfoSource.java	Thu Apr 25 11:28:32 2013 -0400
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginCommandInfoSource.java	Mon Apr 29 11:54:04 2013 -0400
@@ -69,14 +69,18 @@
 
     private static final Logger logger = LoggingUtils.getLogger(PluginCommandInfoSource.class);
 
+    private final UsageStringBuilder usageBuilder;
+
     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());
+        this(new File(internalJarRoot), new File(pluginRootDir), new PluginConfigurationParser(), new UsageStringBuilder());
     }
 
-    PluginCommandInfoSource(File internalJarRoot, File pluginRootDir, PluginConfigurationParser parser) {
+    PluginCommandInfoSource(File internalJarRoot, File pluginRootDir, PluginConfigurationParser parser, UsageStringBuilder usageBuilder) {
+        this.usageBuilder = usageBuilder;
+
         File[] pluginDirs = pluginRootDir.listFiles();
         if (pluginDirs == null) {
             logger.log(Level.SEVERE, "plugin root dir " + pluginRootDir + " does not exist");
@@ -132,9 +136,13 @@
 
             addIfValidPath(bundlePaths, coreJarRoot, command.getDepenedencyBundles());
 
+            String usage = command.getUsage();
+            if (usage == null) {
+                usage = usageBuilder.getUsage(commandName, command.getOptions(), command.getPositionalArguments().toArray(new String[0]));
+            }
             BasicCommandInfo info = new BasicCommandInfo(commandName,
                     command.getDescription(),
-                    command.getUsage(),
+                    usage,
                     command.getOptions(),
                     bundlePaths);
 
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfiguration.java	Thu Apr 25 11:28:32 2013 -0400
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfiguration.java	Mon Apr 29 11:54:04 2013 -0400
@@ -87,16 +87,20 @@
     public static class NewCommand {
 
         private final String commandName;
+        private final String usage;
         private final String description;
-        private String usage;
+        private final List<String> positionalArguments;
         private final Options options;
         private final List<String> additionalResources;
         private final List<String> coreDeps;
 
-        public NewCommand(String name, String description,
-                Options options, List<String> additionalResources, List<String> coreDeps) {
+        public NewCommand(String name, String usage, String description,
+                List<String> positionalArguments, Options options,
+                List<String> additionalResources, List<String> coreDeps) {
             this.commandName = name;
+            this.usage = usage;
             this.description = description;
+            this.positionalArguments = positionalArguments;
             this.options = options;
             this.additionalResources = additionalResources;
             this.coreDeps = coreDeps;
@@ -106,14 +110,25 @@
             return commandName;
         }
 
+        /**
+         * The usage string may be null if no usage string was explicitly
+         * provided. In that case, usage should be "computed" using options and
+         * arguments
+         */
+        public String getUsage() {
+            return usage;
+        }
+
         public String getDescription() {
             return description;
         }
 
-        public String getUsage() {
-            return usage;
+        /** Returns a list of strings indicating positional arguments */
+        public List<String> getPositionalArguments() {
+            return positionalArguments;
         }
 
+        /** Returns options (both optional and required) */
         public Options getOptions() {
             return options;
         }
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParser.java	Thu Apr 25 11:28:32 2013 -0400
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParser.java	Mon Apr 29 11:54:04 2013 -0400
@@ -285,7 +285,7 @@
         }
 
         if (bundles.isEmpty()) {
-            logger.warning("plugin " + pluginName  + " extends the command " + name + " but supplies no bundles");
+            logger.warning("plugin " + pluginName + " extends the command " + name + " but supplies no bundles");
         }
 
         if (name == null) {
@@ -297,7 +297,9 @@
 
     private NewCommand parseNewCommand(String pluginName, Node commandNode) {
         String name = null;
+        String usage = null;
         String description = null;
+        List<String> arguments = new ArrayList<>();
         Options options = new Options();
         List<String> bundles = new ArrayList<>();
         List<String> dependencies = new ArrayList<>();
@@ -307,8 +309,12 @@
             Node node = nodes.item(i);
             if (node.getNodeName().equals("name")) {
                 name = node.getTextContent().trim();
+            } else if (node.getNodeName().equals("usage")) {
+                usage = node.getTextContent().trim();
             } else if (node.getNodeName().equals("description")) {
                 description = node.getTextContent().trim();
+            } else if (node.getNodeName().equals("arguments")) {
+                arguments = parseArguments(pluginName, name, node);
             } else if (node.getNodeName().equals("options")) {
                 options = parseOptions(node);
             } else if (node.getNodeName().equals("bundles")) {
@@ -330,44 +336,39 @@
                     "name='" + name + "', description='" + description + "', options='" + options + "'");
             return null;
         } else {
-            return new NewCommand(name, description, options, bundles, dependencies);
+            return new NewCommand(name, usage, description, arguments, options, bundles, dependencies);
         }
     }
 
     private Collection<String> parseBundles(String pluginName, String commandName, Node bundlesNode) {
-        List<String> bundles = new ArrayList<>();
-        NodeList nodes = bundlesNode.getChildNodes();
+        return parseNodeAsList(pluginName, commandName, bundlesNode, "bundle");
+    }
+
+    private Collection<String> parseDependencies(String pluginName, String commandName, Node dependenciesNode) {
+        return parseNodeAsList(pluginName, commandName, dependenciesNode, "dependency");
+    }
+
+    private List<String> parseArguments(String pluginName, String commandName, Node argumentsNode) {
+        return parseNodeAsList(pluginName, commandName, argumentsNode, "argument");
+    }
+
+    private List<String> parseNodeAsList(String pluginName, String commandName, Node parentNode, String childElementName) {
+        List<String> result = new ArrayList<>();
+        NodeList nodes = parentNode.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);
+            if (node.getNodeName().equals(childElementName)) {
+                String data = node.getTextContent().trim();
+                result.add(data);
             }
         }
 
-        if (bundles.isEmpty()) {
-            logger.warning("plugin " + pluginName + " has an empty bundles element for command " + commandName);
+        if (result.isEmpty()) {
+            logger.warning("plugin " + pluginName + " has an empty " + parentNode.getNodeName()
+                + " element for command " + commandName);
         }
 
-        return bundles;
-    }
-
-    private Collection<String> parseDependencies(String pluginName, String commandName, 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);
-            }
-        }
-
-        if (dependencies.isEmpty()) {
-            logger.warning("plugin " + pluginName + " has an empty dependencies element for command " + commandName);
-        }
-
-        return dependencies;
+        return result;
     }
 
     private Options parseOptions(Node optionsNode) {
@@ -479,8 +480,10 @@
             }
         }
 
-        Option opt = new Option(shortName, longName, Boolean.parseBoolean(argument), description);
-        opt.setArgName(longName != null? longName : shortName);
+        Option opt = new Option(shortName, longName, (argument != null), description);
+        if (argument != null) {
+            opt.setArgName(argument);
+        }
         opt.setRequired(required);
         return opt;
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/UsageStringBuilder.java	Mon Apr 29 11:54:04 2013 -0400
@@ -0,0 +1,106 @@
+/*
+ * Copyright 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 java.util.Collection;
+
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+
+public class UsageStringBuilder {
+
+    /**
+     * Generates a 'usage' string based on the name (of a program) and the
+     * options that program will accept on the command line.
+     *
+     * @param name the program name
+     * @param options the options accepted by this program
+     * @return a String representing the usage.
+     */
+    public String getUsage(String name, Options options, String... positionalArguments) {
+        StringBuilder result = new StringBuilder();
+        result.append(name);
+        // commons-cli has no support for generics, so suppress this warning.
+        @SuppressWarnings("unchecked")
+        Collection<Option> opts = options.getOptions();
+        // iterate twice to handle/print required options first, followed by optional ones
+        for (Option option : opts) {
+            appendOption(result, option, true);
+        }
+        for (Option option : opts) {
+            appendOption(result, option, false);
+        }
+        // print positional arguments last
+        if (positionalArguments != null) {
+            for (String positionalArg : positionalArguments) {
+                result.append(" ").append(positionalArg);
+            }
+        }
+
+        return result.toString();
+    }
+
+    private void appendOption(StringBuilder result, Option option, boolean requiredOptionsOnly) {
+        if (option.isRequired() != requiredOptionsOnly) {
+            return;
+        }
+
+        result.append(" ");
+
+        if (!option.isRequired()) {
+            result.append("[");
+        }
+
+        // prefer to display long form if available
+        if (option.hasLongOpt()) {
+            result.append("--").append(option.getLongOpt());
+        } else {
+            result.append("-").append(option.getOpt());
+        }
+
+        if (option.hasArg()) {
+            result.append(" ").append("<").append(option.getArgName()).append(">");
+        } else if (option.hasOptionalArg()) {
+            result.append("[").append(" ").append("<").append(option.getArgName()).append(">").append("]");
+        }
+
+        if (!option.isRequired()) {
+            result.append("]");
+        }
+    }
+
+}
--- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginCommandInfoSourceTest.java	Thu Apr 25 11:28:32 2013 -0400
+++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginCommandInfoSourceTest.java	Mon Apr 29 11:54:04 2013 -0400
@@ -71,12 +71,14 @@
     private Path pluginRootDir;
     private PluginConfigurationParser parser;
     private PluginConfiguration parserResult;
+    private UsageStringBuilder usageBuilder;
 
     @Before
     public void setUp() throws IOException {
         parser = mock(PluginConfigurationParser.class);
         parserResult = mock(PluginConfiguration.class);
         when(parser.parse(isA(File.class))).thenReturn(parserResult);
+        usageBuilder = mock(UsageStringBuilder.class);
 
         testRoot = Files.createTempDirectory("thermostat");
         pluginRootDir = testRoot.resolve("plugins");
@@ -115,7 +117,7 @@
             Files.createDirectory(pluginDir);
         }
 
-        new PluginCommandInfoSource(jarRootDir.toFile(), pluginRootDir.toFile(), parser);
+        new PluginCommandInfoSource(jarRootDir.toFile(), pluginRootDir.toFile(), parser, usageBuilder);
 
         ArgumentCaptor<File> configFilesCaptor = ArgumentCaptor.forClass(File.class);
         verify(parser, times(pluginDirs.length)).parse(configFilesCaptor.capture());
@@ -131,12 +133,12 @@
     public void verifyMissingConfigurationFileIsHandledCorrectly() throws FileNotFoundException {
         when(parser.parse(isA(File.class))).thenThrow(new FileNotFoundException("test"));
 
-        new PluginCommandInfoSource(jarRootDir.toFile(), pluginRootDir.toFile(), parser);
+        new PluginCommandInfoSource(jarRootDir.toFile(), pluginRootDir.toFile(), parser, usageBuilder);
     }
 
     @Test(expected = CommandInfoNotFoundException.class)
     public void verifyMissingCommandInfo() {
-        PluginCommandInfoSource source = new PluginCommandInfoSource(jarRootDir.toFile(), pluginRootDir.toFile(), parser);
+        PluginCommandInfoSource source = new PluginCommandInfoSource(jarRootDir.toFile(), pluginRootDir.toFile(), parser, usageBuilder);
 
         source.getCommandInfo("TEST");
     }
@@ -161,7 +163,7 @@
 
         when(parserResult.getExtendedCommands()).thenReturn(Arrays.asList(extensions));
 
-        PluginCommandInfoSource source = new PluginCommandInfoSource(jarRootDir.toFile(), pluginRootDir.toFile(), parser);
+        PluginCommandInfoSource source = new PluginCommandInfoSource(jarRootDir.toFile(), pluginRootDir.toFile(), parser, usageBuilder);
 
         CommandInfo info = source.getCommandInfo("command-name");
         assertEquals("command-name", info.getName());
@@ -193,14 +195,14 @@
         NewCommand cmd = mock(NewCommand.class);
         when(cmd.getCommandName()).thenReturn(NAME);
         when(cmd.getDescription()).thenReturn(DESCRIPTION);
-        when(cmd.getUsage()).thenReturn(USAGE);
+        when(usageBuilder.getUsage(NAME, OPTIONS)).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));
 
-        PluginCommandInfoSource source = new PluginCommandInfoSource(jarRootDir.toFile(), pluginRootDir.toFile(), parser);
+        PluginCommandInfoSource source = new PluginCommandInfoSource(jarRootDir.toFile(), pluginRootDir.toFile(), parser, usageBuilder);
 
         CommandInfo result = source.getCommandInfo(NAME);
 
--- a/launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParserTest.java	Thu Apr 25 11:28:32 2013 -0400
+++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/PluginConfigurationParserTest.java	Mon Apr 29 11:54:04 2013 -0400
@@ -187,6 +187,41 @@
     }
 
     @Test
+    public void testArgumentParsing() throws UnsupportedEncodingException {
+        String config = "<?xml version=\"1.0\"?>\n" +
+                "<plugin>\n" +
+                "  <commands>\n" +
+                "    <command type='provides'>\n" +
+                "      <name>test</name>\n" +
+                "      <description>just a test</description>\n" +
+                "      <arguments>\n" +
+                "        <argument>file</argument>\n" +
+                "      </arguments>\n" +
+                "    </command>\n" +
+                "  </commands>\n" +
+                "</plugin>";
+
+        PluginConfiguration result = new PluginConfigurationParser()
+                .parse("test", new ByteArrayInputStream(config.getBytes("UTF-8")));
+
+        assertEquals(0, result.getExtendedCommands().size());
+
+        List<NewCommand> newCommands = result.getNewCommands();
+        assertEquals(1, newCommands.size());
+
+        NewCommand command = newCommands.get(0);
+        assertEquals("test", command.getCommandName());
+        assertEquals("just a test", command.getDescription());
+        assertEquals(null, command.getUsage());
+        Options opts = command.getOptions();
+        assertTrue(opts.getOptions().isEmpty());
+
+        List<String> args = command.getPositionalArguments();
+        assertEquals(1, args.size());
+        assertEquals("file", args.get(0));
+    }
+
+    @Test
     public void testOptionParsing() throws UnsupportedEncodingException {
         String config = "<?xml version=\"1.0\"?>\n" +
                 "<plugin>\n" +
@@ -200,14 +235,12 @@
                 "          <option>\n" +
                 "            <long>exclusive-a</long>\n" +
                 "            <short>a</short>\n" +
-                "            <argument>false</argument>\n" +
                 "            <required>false</required>\n" +
                 "            <description>exclusive option a</description>\n" +
                 "          </option>\n" +
                 "          <option>\n" +
                 "            <long>exclusive-b</long>\n" +
                 "            <short>b</short>\n" +
-                "            <argument>false</argument>\n" +
                 "            <required>false</required>\n" +
                 "            <description>exclusive option b</description>\n" +
                 "          </option>\n" +
@@ -215,7 +248,7 @@
                 "        <option>\n" +
                 "          <long>long</long>\n" +
                 "          <short>l</short>\n" +
-                "          <argument>true</argument>\n" +
+                "          <argument>name</argument>\n" +
                 "          <required>true</required>\n" +
                 "          <description>some required and long option</description>\n" +
                 "        </option>\n" +
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/launcher/src/test/java/com/redhat/thermostat/launcher/internal/UsageStringBuilderTest.java	Mon Apr 29 11:54:04 2013 -0400
@@ -0,0 +1,149 @@
+/*
+ * Copyright 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 org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.junit.Before;
+import org.junit.Test;
+
+public class UsageStringBuilderTest {
+
+    private UsageStringBuilder builder;
+
+    @Before
+    public void setUp() {
+        builder = new UsageStringBuilder();
+    }
+
+    @Test
+    public void verifyUsageWithNoOptions() {
+        Options options = new Options();
+        String usage = builder.getUsage("test", options);
+        assertEquals("test", usage);
+    }
+
+    @Test
+    public void verifyUsageWithSingleShortOption() {
+        Option a = new Option("a", "something");
+        a.setRequired(false);
+        Options options = new Options();
+        options.addOption(a);
+
+        String usage = builder.getUsage("test", options);
+        assertEquals("test [-a]", usage);
+    }
+
+    @Test
+    public void verifyUsageWithShortAndLongOptions() {
+        Options options = new Options();
+
+        Option a = new Option("a", "something");
+        options.addOption(a);
+
+        Option b = new Option("b", "bee", false, "another thing");
+        options.addOption(b);
+
+        String usage = builder.getUsage("test", options);
+        assertEquals("test [--bee] [-a]", usage);
+    }
+
+    @Test
+    public void verifyOptionWithArgument() {
+        Options options = new Options();
+
+        Option a = new Option("a", true, "something");
+        a.setArgName("aaah");
+        options.addOption(a);
+
+        String usage = builder.getUsage("test", options);
+        assertEquals("test [-a <aaah>]", usage);
+    }
+
+    @Test
+    public void verifyRequiredSingleShortOption() {
+        Option a = new Option("a", "something");
+        a.setRequired(true);
+        Options options = new Options();
+        options.addOption(a);
+
+        String usage = builder.getUsage("test", options);
+        assertEquals("test -a", usage);
+    }
+
+    @Test
+    public void verifyRequiredOptionsBeforeOptionalOnes() {
+        Options options = new Options();
+
+        Option a = new Option("a", "something");
+        a.setRequired(true);
+        options.addOption(a);
+
+        Option b = new Option("b", "something");
+        b.setRequired(false);
+        options.addOption(b);
+
+        Option c = new Option("c", "something");
+        c.setRequired(false);
+        options.addOption(c);
+
+        String usage = builder.getUsage("test", options);
+        assertEquals("test -a [-b] [-c]", usage);
+    }
+
+    @Test
+    public void verifyPositionArgumentsAreIncluded() {
+        Options options = new Options();
+
+        String usage = builder.getUsage("test", options, "agent-id", "vm-id");
+        assertEquals("test agent-id vm-id", usage);
+    }
+
+    @Test
+    public void verifyPositionArgumentsAreDisplayedLast() {
+        Options options = new Options();
+
+        Option a = new Option("a", "something");
+        a.setRequired(true);
+        options.addOption(a);
+
+        String usage = builder.getUsage("test", options, "agent-id", "vm-id");
+        assertEquals("test -a agent-id vm-id", usage);
+    }
+}
--- a/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/SwingThreadTimelineView.java	Thu Apr 25 11:28:32 2013 -0400
+++ b/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/SwingThreadTimelineView.java	Mon Apr 29 11:54:04 2013 -0400
@@ -52,9 +52,11 @@
 
 import com.redhat.thermostat.client.swing.ComponentVisibleListener;
 import com.redhat.thermostat.client.swing.SwingComponent;
+
 import com.redhat.thermostat.client.swing.components.timeline.TimelineRulerHeader;
-import com.redhat.thermostat.client.swing.components.timeline.TimelineUtils;
+
 import com.redhat.thermostat.common.model.LongRange;
+
 import com.redhat.thermostat.thread.client.common.Timeline;
 import com.redhat.thermostat.thread.client.common.view.ThreadTimelineView;
 
@@ -113,7 +115,8 @@
         scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
 
         long now = System.currentTimeMillis();
-        header = new ThreadTimelineHeader(new LongRange(now, now + TimelineUtils.STEP), scrollPane);
+        header = new ThreadTimelineHeader(new LongRange(now, now + TimelineRulerHeader.DEFAULT_INCREMENT_IN_MILLIS),
+                                          scrollPane);
         scrollPane.setColumnHeaderView(header);
 
         ScrollChangeListener listener = new ScrollChangeListener();
@@ -134,7 +137,7 @@
 
                     int extent = scrollBar.getVisibleAmount();
                     int min = scrollBar.getMinimum();
-                    int max = component.getWidth() + (2 * TimelineUtils.INC);
+                    int max = component.getWidth() + (int) (2 * header.getUnitIncrementInMillis());
 
                     scrollBar.setValues(max - extent, extent, min, max);
                 }
@@ -148,10 +151,15 @@
         SwingUtilities.invokeLater(new Runnable() {
             @Override
             public void run() {
-                range.setMax(range.getMax() + (2 * TimelineUtils.STEP));
+                range.setMax(range.getMax() + (int) (2 * header.getUnitIncrementInMillis()));
                 chartModel.removeAllElements();
                 for (Timeline timeline : timelines) {
-                    chartModel.addElement(new TimelineComponent(range, timeline, scrollPane));
+                    
+                    TimelineComponent timelineComp = new TimelineComponent(range, timeline, scrollPane);
+                    timelineComp.setUnitIncrementInMillis(header.getUnitIncrementInMillis());
+                    timelineComp.setUnitIncrementInPixels(header.getUnitIncrementInPixels());
+                    
+                    chartModel.addElement(timelineComp);
                 }
                 header.getRange().setMin(range.getMin());
                 header.getRange().setMax(range.getMax());
--- a/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/timeline/TimelineComponent.java	Thu Apr 25 11:28:32 2013 -0400
+++ b/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/timeline/TimelineComponent.java	Mon Apr 29 11:54:04 2013 -0400
@@ -56,6 +56,7 @@
 
 import com.redhat.thermostat.client.swing.GraphicsUtils;
 import com.redhat.thermostat.client.swing.components.GradientPanel;
+
 import com.redhat.thermostat.client.swing.components.timeline.TimelineUtils;
 
 import com.redhat.thermostat.client.ui.Palette;
@@ -74,13 +75,29 @@
     private Timeline timeline;
     private JScrollPane scrollPane;
     private LongRange range;
-    public TimelineComponent(LongRange range, Timeline timeline, JScrollPane scrollPane) {
+    
+    private long millsUnitIncrement;
+    private int pixelUnitIncrement;
+    
+    public TimelineComponent(LongRange range, Timeline timeline, JScrollPane scrollPane)
+    {
         super(Palette.LIGHT_GRAY.getColor(), Palette.WHITE.getColor());
         this.range = range;
         this.scrollPane = scrollPane;
         this.timeline = timeline;
+        
+        millsUnitIncrement = 1_000;
+        pixelUnitIncrement = 20;
     }
 
+    public void setUnitIncrementInPixels(int increment) {
+        this.pixelUnitIncrement = increment;
+    }
+    
+    public void setUnitIncrementInMillis(long increment) {
+        this.millsUnitIncrement = increment;
+    }
+    
     public void setSelected(boolean selected) {
         this.selected = selected;
     }
@@ -98,10 +115,10 @@
             graphics.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
         }
         
-        int height = getHeight();
         int currentValue = scrollPane.getHorizontalScrollBar().getValue();
-        int totalInc = TimelineUtils.drawMarks(range, graphics, bounds, currentValue, getWidth(), height);
-        
+        int totalInc = pixelUnitIncrement;
+        TimelineUtils.drawMarks(range, graphics, bounds, currentValue, false, totalInc);
+
         drawBoldMarks(graphics, currentValue, bounds, totalInc);
         Color lastColor = drawTimeline(graphics, currentValue, bounds);
         
@@ -174,8 +191,8 @@
     
     private void drawBoldMarks(Graphics2D graphics, int currentValue, Rectangle bounds, int totalInc) {
 
-        long round = range.getMin() % 10000;
-        int shift = (int) (round / TimelineUtils.STEP) * totalInc;
+        long round = range.getMin() % (10 * millsUnitIncrement);
+        int shift = (int) (round / millsUnitIncrement) * totalInc;
         
         int lowerBound = bounds.x - (4 * totalInc);
         int x = ((bounds.x - currentValue) - shift);
@@ -204,7 +221,12 @@
     
     @Override
     public int getWidth() {
-        return TimelineUtils.calculateWidth(range);
+         
+        long divisor = millsUnitIncrement / pixelUnitIncrement;
+        
+        long span = range.getMax() - range.getMin();
+        int width = (int) (span / divisor);
+        return width;
     }
     
     @Override
@@ -217,62 +239,5 @@
     public Dimension getPreferredSize() {
         return new Dimension(getWidth(), getHeight());
     }
-        
-    public static void main(String[] args) {
-        SwingUtilities.invokeLater(new Runnable() {
-            @Override
-            public void run() {
-                
-                LongRange range = new LongRange(50000, 2000000); // 31558464000L
-                Timeline timeline = new Timeline("Test", 1000);
-                timeline.add(new TimelineInfo(Palette.THERMOSTAT_BLU, range.getMax() - 1000));
-                timeline.add(new TimelineInfo(Palette.TUNDRA_GREEN, 152000));
-                timeline.add(new TimelineInfo(Palette.DIRTY_CYAN, 63000));
-                timeline.add(new TimelineInfo(Palette.THERMOSTAT_BLU, 62000));
-                timeline.add(new TimelineInfo(Palette.THERMOSTAT_RED, 60210));
-                timeline.add(new TimelineInfo(Palette.THERMOSTAT_BLU, 51299));
-
-                final JFrame frame = new JFrame();
-                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
-                
-                
-                final JScrollPane scrollPane = new JScrollPane();
-                DefaultListModel<TimelineComponent> chartModel = new DefaultListModel<>();
-                for (int i = 0; i < 100; i++) {
-                    chartModel.addElement(new TimelineComponent(range, timeline, scrollPane));
-                }
-                
-                final JList<TimelineComponent> stuff = new JList<>(chartModel);
-                stuff.setCellRenderer(new TimelineCellRenderer());
-                
-                scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
-                scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
-                scrollPane.getHorizontalScrollBar().setUnitIncrement(TimelineUtils.INC);
-                
-                final ThreadTimelineHeader header = new ThreadTimelineHeader(range, scrollPane);
-                
-                scrollPane.setColumnHeaderView(header);
-                scrollPane.setViewportView(stuff);
-                scrollPane.getVerticalScrollBar().getModel().addChangeListener(new ChangeListener() {
-                    @Override
-                    public void stateChanged(ChangeEvent e) {
-                        scrollPane.repaint();
-                    }
-                });
-                
-                scrollPane.getHorizontalScrollBar().getModel().addChangeListener(new ChangeListener() {
-                    
-                    @Override
-                    public void stateChanged(ChangeEvent e) {
-                        scrollPane.repaint();
-                    }
-                });
-                
-                frame.add(scrollPane);
-                frame.setSize(300, 300);
-                frame.setVisible(true);
-            }
-        });
-    }     
 }
 
--- a/vm-heap-analysis/distribution/plugin.xml	Thu Apr 25 11:28:32 2013 -0400
+++ b/vm-heap-analysis/distribution/plugin.xml	Mon Apr 29 11:54:04 2013 -0400
@@ -47,14 +47,14 @@
         <option>
           <long>hostId</long>
           <short>a</short>
-          <argument>true</argument>
+          <argument>host</argument>
           <required>true</required>
           <description>the ID of the host to monitor</description>
         </option>
         <option>
           <long>vmId</long>
           <short>v</short>
-          <argument>true</argument>
+          <argument>vm</argument>
           <required>true</required>
           <description>the ID of the VM to monitor</description>
         </option>
@@ -91,18 +91,21 @@
     <command>
       <name>find-objects</name>
       <description>finds objects in a heapdump</description>
+      <arguments>
+        <argument>pattern</argument>
+      </arguments>
       <options>
         <option>
           <long>heapId</long>
           <short>h</short>
-          <argument>true</argument>
+          <argument>heap</argument>
           <required>true</required>
           <description>the ID of the heapdump to analyze</description>
         </option>
         <option>
           <long>limit</long>
           <short>L</short>
-          <argument>true</argument>
+          <argument>limit</argument>
           <required>false</required>
           <description>limit search to top N results, defaults to 10</description>
         </option>
@@ -143,21 +146,20 @@
         <option>
           <long>heapId</long>
           <short>h</short>
-          <argument>true</argument>
+          <argument>heap</argument>
           <required>true</required>
           <description>the ID of the heapdump to analyze</description>
         </option>
         <option>
           <long>objectId</long>
           <short>o</short>
-          <argument>true</argument>
+          <argument>heap</argument>
           <required>true</required>
           <description>the ID of the object to query</description>
         </option>
         <option>
           <long>all</long>
           <short>a</short>
-          <argument>false</argument>
           <required>false</required>
           <description>finds all paths to GC roots</description>
         </option>
@@ -198,14 +200,14 @@
         <option>
           <long>hostId</long>
           <short>a</short>
-          <argument>true</argument>
+          <argument>host</argument>
           <required>false</required>
           <description>the ID of the host to monitor</description>
         </option>
         <option>
           <long>vmId</long>
           <short>v</short>
-          <argument>true</argument>
+          <argument>vm</argument>
           <required>false</required>
           <description>the ID of the VM to monitor</description>
         </option>
@@ -246,14 +248,14 @@
         <option>
           <long>heapId</long>
           <short>h</short>
-          <argument>true</argument>
+          <argument>heap</argument>
           <required>true</required>
           <description>the ID of the heapdump to analyze</description>
         </option>
         <option>
           <long>objectId</long>
           <short>o</short>
-          <argument>true</argument>
+          <argument>object</argument>
           <required>true</required>
           <description>the ID of the object to query</description>
         </option>
@@ -294,14 +296,14 @@
         <option>
           <long>heapId</long>
           <short>h</short>
-          <argument>true</argument>
+          <argument>heap</argument>
           <required>true</required>
           <description>the ID of the heapdump to analyze</description>
         </option>
         <option>
           <long>file</long>
           <short>f</short>
-          <argument>true</argument>
+          <argument>filename</argument>
           <required>true</required>
           <description>the file name to save to</description>
         </option>
@@ -342,7 +344,7 @@
         <option>
           <long>heapId</long>
           <short>h</short>
-          <argument>true</argument>
+          <argument>heap</argument>
           <required>true</required>
           <description>the ID of the heapdump to analyze</description>
         </option>