changeset 908:1e51015e63e5

Allow plugins to dynamically contribute output to vm-stat command This commit follows the previous commits that created agent and common bundles for the plugins we include with Thermostat. Currently the vm-stat command uses output from the VmCpuStatDAO and VmMemoryStatDAO, which are now contained in plugins. In order to not have core Thermostat rely on the presence of plugins, this commit introduces a VMStatPrintDelegate interface which plugins can implement to dynamically provide output to the vm-stat command. The end result is the same vm-stat output as before, but without dependencies on the vm-cpu-common and vm-memory-common bundles. In addition, other plugins can contribute columns of data to the command as well. Reviewed-by: omajid Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-January/005031.html
author Elliott Baron <ebaron@redhat.com>
date Wed, 09 Jan 2013 14:59:30 -0500
parents 0289117ee9ef
children 2930e0c260cc
files client/cli/pom.xml client/cli/src/main/java/com/redhat/thermostat/client/cli/VMStatPrintDelegate.java client/cli/src/main/java/com/redhat/thermostat/client/cli/internal/LocaleResources.java client/cli/src/main/java/com/redhat/thermostat/client/cli/internal/VMStatCommand.java client/cli/src/main/java/com/redhat/thermostat/client/cli/internal/VMStatPrinter.java client/cli/src/main/resources/com/redhat/thermostat/client/cli/strings.properties client/cli/src/test/java/com/redhat/thermostat/client/cli/internal/ActivatorTest.java client/cli/src/test/java/com/redhat/thermostat/client/cli/internal/VmStatCommandTest.java common/core/src/main/java/com/redhat/thermostat/test/StubBundleContext.java distribution/config/commands/vm-stat.properties distribution/pom.xml vm-cpu/client-cli/pom.xml vm-cpu/client-cli/src/main/java/com/redhat/thermostat/vm/cpu/client/cli/internal/Activator.java vm-cpu/client-cli/src/main/java/com/redhat/thermostat/vm/cpu/client/cli/internal/LocaleResources.java vm-cpu/client-cli/src/main/java/com/redhat/thermostat/vm/cpu/client/cli/internal/VmCpuStatPrintDelegate.java vm-cpu/client-cli/src/main/resources/com/redhat/thermostat/vm/cpu/client/cli/strings.properties vm-cpu/client-cli/src/test/java/com/redhat/thermostat/vm/cpu/client/cli/internal/ActivatorTest.java vm-cpu/client-cli/src/test/java/com/redhat/thermostat/vm/cpu/client/cli/internal/LocaleResourcesTest.java vm-cpu/client-cli/src/test/java/com/redhat/thermostat/vm/cpu/client/cli/internal/VmCpuStatPrintDelegateTest.java vm-cpu/pom.xml vm-memory/client-cli/pom.xml vm-memory/client-cli/src/main/java/com/redhat/thermostat/vm/memory/client/cli/internal/Activator.java vm-memory/client-cli/src/main/java/com/redhat/thermostat/vm/memory/client/cli/internal/LocaleResources.java vm-memory/client-cli/src/main/java/com/redhat/thermostat/vm/memory/client/cli/internal/VmMemoryStatPrintDelegate.java vm-memory/client-cli/src/main/resources/com/redhat/thermostat/vm/memory/client/cli/strings.properties vm-memory/client-cli/src/test/java/com/redhat/thermostat/vm/memory/client/cli/internal/ActivatorTest.java vm-memory/client-cli/src/test/java/com/redhat/thermostat/vm/memory/client/cli/internal/LocaleResourcesTest.java vm-memory/client-cli/src/test/java/com/redhat/thermostat/vm/memory/client/cli/internal/VmMemoryStatPrintDelegateTest.java vm-memory/pom.xml
diffstat 29 files changed, 1622 insertions(+), 294 deletions(-) [+]
line wrap: on
line diff
--- a/client/cli/pom.xml	Tue Jan 08 16:50:11 2013 -0500
+++ b/client/cli/pom.xml	Wed Jan 09 14:59:30 2013 -0500
@@ -99,18 +99,12 @@
     <dependency>
       <groupId>org.osgi</groupId>
       <artifactId>org.osgi.core</artifactId>
+      <scope>provided</scope>
     </dependency>
-    <!-- Only temporary dependency -->
     <dependency>
-      <groupId>com.redhat.thermostat</groupId>
-      <artifactId>thermostat-vm-cpu-common</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-    <!-- Only temporary dependency -->
-    <dependency>
-      <groupId>com.redhat.thermostat</groupId>
-      <artifactId>thermostat-vm-memory-common</artifactId>
-      <version>${project.version}</version>
+      <groupId>org.osgi</groupId>
+      <artifactId>org.osgi.compendium</artifactId>
+      <scope>provided</scope>
     </dependency>
   </dependencies>
 
@@ -125,17 +119,15 @@
             <Bundle-Vendor>Red Hat, Inc.</Bundle-Vendor>
             <Bundle-Activator>com.redhat.thermostat.client.cli.internal.Activator</Bundle-Activator>
             <Bundle-SymbolicName>com.redhat.thermostat.client.cli</Bundle-SymbolicName>
+            <Export-Package>
+              com.redhat.thermostat.client.cli,
+            </Export-Package>
             <Private-Package>
               META_INF.services,
               com.redhat.thermostat.client.cli.internal,
             </Private-Package>
             <!-- Do not autogenerate uses clauses in Manifests -->
             <_nouses>true</_nouses>
-            <!-- Remove this later -->
-            <Import-Package>
-              *,com.redhat.thermostat.vm.cpu.common;resolution:=optional,
-              com.redhat.thermostat.vm.memory.common;resolution:=optional
-            </Import-Package>
           </instructions>
         </configuration>
       </plugin>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/cli/src/main/java/com/redhat/thermostat/client/cli/VMStatPrintDelegate.java	Wed Jan 09 14:59:30 2013 -0500
@@ -0,0 +1,76 @@
+/*
+ * 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.client.cli;
+
+import java.util.List;
+
+import com.redhat.thermostat.common.Ordered;
+import com.redhat.thermostat.common.dao.VmRef;
+import com.redhat.thermostat.storage.model.TimeStampedPojo;
+
+/**
+ * This interface should be implemented by plug-ins that would like to
+ * contribute data to the output of the vm-stat command.
+ */
+public interface VMStatPrintDelegate extends Ordered {
+    
+    /**
+     * Returns statistics gathered by this plug-in newer than the specified
+     * time stamp.
+     * @param ref - the VM whose statistics to return
+     * @param timeStampSince - the earliest time stamp to return statistics for
+     * @return a list of statistics newer than the time stamp
+     */
+    public List<? extends TimeStampedPojo> getLatestStats(VmRef ref, long timeStampSince);
+    
+    /**
+     * Returns header names for columns this plug-in wishes to add to the 
+     * vm-stat command.
+     * @param stat - the first stat returned by {@link #getLatestStats(VmRef, long)}
+     * @return a list of column headers to append to vm-stat output
+     */
+    public List<String> getHeaders(TimeStampedPojo stat);
+    
+    /**
+     * Returns a row of data for the specified statistic that corresponds to
+     * the columns returned by {@link #getHeaders(TimeStampedPojo)}.
+     * @param stat - the statistic to generate output for
+     * @return a row of text for this statistic separated by column
+     */
+    public List<String> getStatRow(TimeStampedPojo stat);
+    
+}
--- a/client/cli/src/main/java/com/redhat/thermostat/client/cli/internal/LocaleResources.java	Tue Jan 08 16:50:11 2013 -0500
+++ b/client/cli/src/main/java/com/redhat/thermostat/client/cli/internal/LocaleResources.java	Wed Jan 09 14:59:30 2013 -0500
@@ -70,8 +70,6 @@
     COLUMN_HEADER_VM_NAME,
     COLUMN_HEADER_VM_STATUS,
     COLUMN_HEADER_TIME,
-    COLUMN_HEADER_CPU_PERCENT,
-    COLUMN_HEADER_MEMORY_PATTERN,
 
     VM_STOP_TIME_RUNNING,
     VM_STATUS_ALIVE,
--- a/client/cli/src/main/java/com/redhat/thermostat/client/cli/internal/VMStatCommand.java	Tue Jan 08 16:50:11 2013 -0500
+++ b/client/cli/src/main/java/com/redhat/thermostat/client/cli/internal/VMStatCommand.java	Wed Jan 09 14:59:30 2013 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012 Red Hat, Inc.
+ * Copyright 2013 Red Hat, Inc.
  *
  * This file is part of Thermostat.
  *
@@ -37,71 +37,81 @@
 package com.redhat.thermostat.client.cli.internal;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+
+import com.redhat.thermostat.client.cli.VMStatPrintDelegate;
 import com.redhat.thermostat.common.ApplicationService;
+import com.redhat.thermostat.common.OrderedComparator;
 import com.redhat.thermostat.common.Timer;
 import com.redhat.thermostat.common.cli.CommandContext;
 import com.redhat.thermostat.common.cli.CommandException;
 import com.redhat.thermostat.common.cli.HostVMArguments;
 import com.redhat.thermostat.common.cli.SimpleCommand;
 import com.redhat.thermostat.common.dao.VmRef;
-import com.redhat.thermostat.common.locale.Translate;
 import com.redhat.thermostat.common.utils.LoggingUtils;
-import com.redhat.thermostat.common.utils.OSGIUtils;
-import com.redhat.thermostat.vm.cpu.common.VmCpuStatDAO;
-import com.redhat.thermostat.vm.memory.common.VmMemoryStatDAO;
 
 public class VMStatCommand extends SimpleCommand {
 
-    private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
-
     private static final Logger log = LoggingUtils.getLogger(VMStatCommand.class);
-
     private static final String CMD_NAME = "vm-stat";
-
-    private OSGIUtils serviceProvider;
-
+    
+    private List<VMStatPrintDelegate> delegates;
+    private BundleContext context;
+    
     public VMStatCommand() {
-        this(OSGIUtils.getInstance());
+        this(FrameworkUtil.getBundle(VMStatCommand.class).getBundleContext());
     }
 
-    VMStatCommand(OSGIUtils serviceProvider) {
-        this.serviceProvider = serviceProvider;
+    VMStatCommand(BundleContext context) {
+        this.context = context;
+        delegates = new CopyOnWriteArrayList<>();
+        ServiceTracker tracker = new ServiceTracker(context, VMStatPrintDelegate.class.getName(), null) {
+
+            public Object addingService(ServiceReference reference) {
+                VMStatPrintDelegate delegate = (VMStatPrintDelegate) super.addingService(reference);
+                delegates.add(delegate);
+                return delegate;
+            };
+
+            public void removedService(ServiceReference reference, Object service) {
+                delegates.remove(service);
+                super.removedService(reference, service);
+            };
+
+        };
+        tracker.open();
     }
 
     @Override
     public void run(final CommandContext ctx) throws CommandException {
-        VmCpuStatDAO vmCpuStatDAO = serviceProvider.getServiceAllowNull(VmCpuStatDAO.class);
-        if (vmCpuStatDAO == null) {
-            throw new CommandException(translator.localize(LocaleResources.VM_CPU_SERVICE_NOT_AVAILABLE));
-        }
-
-        VmMemoryStatDAO vmMemoryStatDAO = serviceProvider.getServiceAllowNull(VmMemoryStatDAO.class);
-        if (vmMemoryStatDAO == null) {
-            throw new CommandException(translator.localize(LocaleResources.VM_MEMORY_SERVICE_NOT_AVAILABLE));
-        }
-
         HostVMArguments hostVMArgs = new HostVMArguments(ctx.getArguments());
         VmRef vm = hostVMArgs.getVM();
-        final VMStatPrinter statPrinter = new VMStatPrinter(vm, vmCpuStatDAO, vmMemoryStatDAO, ctx.getConsole().getOutput());
+        // Pass a copy of the delegates list to the printer
+        final VMStatPrinter statPrinter = new VMStatPrinter(vm, new ArrayList<>(delegates), ctx.getConsole().getOutput());
         statPrinter.printStats();
         boolean continuous = ctx.getArguments().hasArgument("continuous");
         if (continuous) {
             startContinuousStats(ctx, statPrinter);
         }
-
-        serviceProvider.ungetService(VmMemoryStatDAO.class, vmMemoryStatDAO);
-        serviceProvider.ungetService(VmCpuStatDAO.class, vmCpuStatDAO);
     }
 
     private void startContinuousStats(final CommandContext ctx, final VMStatPrinter statPrinter) {
 
         final CountDownLatch latch = new CountDownLatch(1);
-        ApplicationService appSvc = serviceProvider.getService(ApplicationService.class);
+        ServiceReference ref = context.getServiceReference(ApplicationService.class.getName());
+        ApplicationService appSvc = (ApplicationService) context.getService(ref);
         Timer timer = appSvc.getTimerFactory().createTimer();
         timer.setDelay(1);
         timer.setInitialDelay(1);
@@ -133,6 +143,8 @@
         } catch (InterruptedException e) {
             // Return immediately.
         }
+        
+        context.ungetService(ref);
     }
 
     @Override
--- a/client/cli/src/main/java/com/redhat/thermostat/client/cli/internal/VMStatPrinter.java	Tue Jan 08 16:50:11 2013 -0500
+++ b/client/cli/src/main/java/com/redhat/thermostat/client/cli/internal/VMStatPrinter.java	Wed Jan 09 14:59:30 2013 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012 Red Hat, Inc.
+ * Copyright 2013 Red Hat, Inc.
  *
  * This file is part of Thermostat.
  *
@@ -38,70 +38,102 @@
 
 import java.io.PrintStream;
 import java.text.DateFormat;
-import java.text.DecimalFormat;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.NoSuchElementException;
 
-import com.redhat.thermostat.common.Size;
+import com.redhat.thermostat.client.cli.VMStatPrintDelegate;
+import com.redhat.thermostat.common.OrderedComparator;
 import com.redhat.thermostat.common.cli.TableRenderer;
 import com.redhat.thermostat.common.dao.VmRef;
 import com.redhat.thermostat.common.locale.Translate;
 import com.redhat.thermostat.storage.model.TimeStampedPojo;
 import com.redhat.thermostat.storage.model.TimeStampedPojoComparator;
 import com.redhat.thermostat.storage.model.TimeStampedPojoCorrelator;
-import com.redhat.thermostat.storage.model.VmCpuStat;
-import com.redhat.thermostat.storage.model.VmMemoryStat;
-import com.redhat.thermostat.vm.cpu.common.VmCpuStatDAO;
-import com.redhat.thermostat.vm.memory.common.VmMemoryStatDAO;
 
 class VMStatPrinter {
 
     private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
 
-    private static final String CPU_PERCENT = translator.localize(LocaleResources.COLUMN_HEADER_CPU_PERCENT);
     private static final String TIME = translator.localize(LocaleResources.COLUMN_HEADER_TIME);
 
     private VmRef vm;
-    private VmCpuStatDAO vmCpuStatDAO;
-    private VmMemoryStatDAO vmMemoryStatDAO;
+    private List<VMStatPrintDelegate> delegates;
     private PrintStream out;
-    private TimeStampedPojoCorrelator correlator = new TimeStampedPojoCorrelator(2);
+    private TimeStampedPojoCorrelator correlator;
     private TableRenderer table;
-    private int numSpaces;
+    private int numCols;
+    private Map<VMStatPrintDelegate, DelegateInfo> delegateInfo;
 
-    private long lastCpuStatTimeStamp = Long.MIN_VALUE;
-    private long lastMemoryStatTimeStamp = Long.MIN_VALUE;
-
-    VMStatPrinter(VmRef vm, VmCpuStatDAO vmCpuStatDAO, VmMemoryStatDAO vmMemoryStatDAO, PrintStream out) {
+    VMStatPrinter(VmRef vm, List<VMStatPrintDelegate> delegates, PrintStream out) {
         this.vm = vm;
-        this.vmCpuStatDAO = vmCpuStatDAO;
-        this.vmMemoryStatDAO = vmMemoryStatDAO;
+        this.delegates = delegates;
         this.out = out;
+        int numDelegates = delegates.size();
+        this.delegateInfo = new HashMap<>();
+        this.correlator = new TimeStampedPojoCorrelator(numDelegates);
+        
+        // Sort the delegates list
+        Collections.sort(delegates, new OrderedComparator<>());
+        
+        for (VMStatPrintDelegate delegate : delegates) {
+            DelegateInfo info = new DelegateInfo();
+            info.lastTimeStamp = Long.MIN_VALUE;
+            delegateInfo.put(delegate, info);
+        }
     }
 
     void printStats() {
-        List<VmCpuStat> cpuStats = vmCpuStatDAO.getLatestVmCpuStats(vm, lastCpuStatTimeStamp);
-        List<VmMemoryStat> memStats = vmMemoryStatDAO.getLatestVmMemoryStats(vm, lastMemoryStatTimeStamp);
-
-        lastCpuStatTimeStamp = getLatestTimeStamp(lastCpuStatTimeStamp, cpuStats);
-        lastMemoryStatTimeStamp = getLatestTimeStamp(lastMemoryStatTimeStamp, memStats);
-
-        printStats(cpuStats, memStats);
+        List<List<? extends TimeStampedPojo>> allStats = new ArrayList<>();
+        List<String> allHeaders = new ArrayList<>();
+        allHeaders.add(TIME);
+        
+        // Copy since we can remove elements in loop body
+        List<VMStatPrintDelegate> delegatesCopy = new ArrayList<>(delegates);
+        for (VMStatPrintDelegate delegate : delegatesCopy) {
+            long timeStamp = delegateInfo.get(delegate).lastTimeStamp;
+            List<? extends TimeStampedPojo> latestStats = delegate.getLatestStats(vm, timeStamp);
+            if (latestStats == null || latestStats.isEmpty()) {
+                // Skipping delegate
+                delegates.remove(delegate);
+            }
+            else {
+                List<String> headers = delegate.getHeaders(latestStats.get(0));
+                if (headers == null || headers.isEmpty()) {
+                    // Skipping delegate
+                    delegates.remove(delegate);
+                }
+                else {
+                    DelegateInfo info = delegateInfo.get(delegate);
+                    info.colsPerDelegate = headers.size();
+                    allHeaders.addAll(headers);
+                    allStats.add(latestStats);
+                    info.lastTimeStamp = getLatestTimeStamp(timeStamp, latestStats);
+                }
+            }
+        }
+        
+        printStats(allStats, allHeaders);
     }
 
     void printUpdatedStats() {
         correlator.clear();
-        List<VmCpuStat> cpuStats = vmCpuStatDAO.getLatestVmCpuStats(vm, lastCpuStatTimeStamp);
-        List<VmMemoryStat> memStats = vmMemoryStatDAO.getLatestVmMemoryStats(vm, lastMemoryStatTimeStamp);
+        
+        List<List<? extends TimeStampedPojo>> allStats = new ArrayList<>();
+        for (VMStatPrintDelegate delegate : delegates) {
+            DelegateInfo info = delegateInfo.get(delegate);
+            List<? extends TimeStampedPojo> latestStats = delegate.getLatestStats(vm, info.lastTimeStamp);
+            allStats.add(latestStats);
+            info.lastTimeStamp = getLatestTimeStamp(info.lastTimeStamp, latestStats);
+        }
 
-        lastCpuStatTimeStamp = getLatestTimeStamp(lastCpuStatTimeStamp, cpuStats);
-        lastMemoryStatTimeStamp = getLatestTimeStamp(lastMemoryStatTimeStamp, memStats);
-
-        correlate(cpuStats, memStats);
+        correlate(allStats);
         printUpdatedStatsImpl();
     }
 
@@ -113,103 +145,79 @@
         }
     }
 
-    private void printStats(List<VmCpuStat> cpuStats, List<VmMemoryStat> memStats) {
-        correlate(cpuStats, memStats);
-        numSpaces = getNumSpaces(memStats);
-        int numColumns = numSpaces + 2;
-        table = new TableRenderer(numColumns);
-        printHeaders(memStats, numSpaces, numColumns, table);
+    private void printStats(List<List<? extends TimeStampedPojo>> allStats, List<String> headers) {
+        correlate(allStats);
+        numCols = headers.size();
+        table = new TableRenderer(numCols);
+        printHeaders(table, headers);
         printUpdatedStatsImpl();
     }
 
-    private void printStats(int numSpaces, TableRenderer table, Iterator<TimeStampedPojoCorrelator.Correlation> i) {
-
-        TimeStampedPojoCorrelator.Correlation correlation = i.next();
+    private void printStats(TableRenderer table, Iterator<TimeStampedPojoCorrelator.Correlation> iter) {
+        TimeStampedPojoCorrelator.Correlation correlation = iter.next();
 
-        VmCpuStat cpuStat = (VmCpuStat) correlation.get(0);
-        DecimalFormat format = new DecimalFormat("#0.0");
-        String cpuLoad = cpuStat != null ? format.format(cpuStat.getCpuLoad()) : "";
-
+        String[] line = new String[numCols];
         DateFormat dateFormat = DateFormat.getTimeInstance();
         String time = dateFormat.format(new Date(correlation.getTimeStamp()));
-
-        String[] memoryUsage = getMemoryUsage((VmMemoryStat) correlation.get(1), numSpaces);
-
-        String[] line = new String[numSpaces + 2];
-        System.arraycopy(memoryUsage, 0, line, 2, numSpaces);
         line[0] = time;
-        line[1] = cpuLoad;
+        
+        int off = 1; // time is first index
+        for (int i = 0; i < delegates.size(); i++) {
+            TimeStampedPojo stat = correlation.get(i);
+            VMStatPrintDelegate delegate = delegates.get(i);
+            if (stat == null) {
+                // Fill with blanks
+                DelegateInfo info = delegateInfo.get(delegate);
+                Arrays.fill(line, off, off + info.colsPerDelegate, "");
+                off += info.colsPerDelegate;
+            }
+            else {
+                List<String> data = delegate.getStatRow(stat);
+                if (data == null) {
+                    throw new NullPointerException("Returned null stat row");
+                }
+                else if (data.size() != delegateInfo.get(delegate).colsPerDelegate) {
+                    throw new IllegalStateException("Delegate "
+                            + delegate.toString() + " provided "
+                            + delegateInfo.get(delegate).colsPerDelegate
+                            + " column headers, but only " + data.size()
+                            + " stat row values");
+                }
+                else {
+                    System.arraycopy(data.toArray(), 0, line, off, data.size());
+                    off += data.size();
+                }
+            }
+        }
+        
         table.printLine(line);
     }
 
-    private void printHeaders(List<VmMemoryStat> memStats, int numSpaces, int numColumns, TableRenderer table) {
-        String[] spacesNames = getSpacesNames(memStats, numSpaces);
-        String[] headers = new String[numColumns];
-        headers[0] = TIME;
-        headers[1] = CPU_PERCENT;
-        System.arraycopy(spacesNames, 0, headers, 2, numSpaces);
-        table.printLine(headers);
-    }
-
-    private String[] getMemoryUsage(VmMemoryStat vmMemoryStat, int numSpaces) {
-        String[] memoryUsage = new String[numSpaces];
-        if (vmMemoryStat == null) {
-            Arrays.fill(memoryUsage, "");
-            return memoryUsage;
-        }
-        int i = 0;
-        for (VmMemoryStat.Generation gen : vmMemoryStat.getGenerations()) {
-            for (VmMemoryStat.Space space : gen.getSpaces()) {
-                memoryUsage[i] = Size.bytes(space.getUsed()).toString();
-                i++;
-            }
-        }
-        return memoryUsage;
+    private void printHeaders(TableRenderer table, List<String> headers) {
+        table.printLine(headers.toArray(new String[headers.size()]));
     }
 
-    private String[] getSpacesNames(List<VmMemoryStat> memStats, int numSpaces) {
-        if (numSpaces < 1) {
-            return new String[0];
-        }
-        String[] spacesNames = new String[numSpaces];
-        VmMemoryStat stat = memStats.get(0);
-        int i = 0;
-        for (VmMemoryStat.Generation gen : stat.getGenerations()) {
-            for (VmMemoryStat.Space space : gen.getSpaces()) {
-                spacesNames[i] = translator.localize(LocaleResources.COLUMN_HEADER_MEMORY_PATTERN, space.getName());
-                i++;
+    private void correlate(List<List<? extends TimeStampedPojo>> allStats) {
+        int count = 0;
+        for (List<? extends TimeStampedPojo> stats : allStats) {
+            for(TimeStampedPojo cpuStat : stats) {
+                correlator.add(count, cpuStat);
             }
-        }
-        return spacesNames;
-    }
-
-    private int getNumSpaces(List<VmMemoryStat> memStats) {
-        if (memStats.size() < 1) {
-            return 0;
-        }
-        VmMemoryStat stat = memStats.get(0);
-        int numSpaces = 0;
-        for (VmMemoryStat.Generation gen : stat.getGenerations()) {
-            numSpaces += gen.getSpaces().length;
-        }
-        return numSpaces;
-    }
-
-    private void correlate(List<VmCpuStat> cpuStats, List<VmMemoryStat> memStats) {
-        for(VmCpuStat cpuStat : cpuStats) {
-            correlator.add(0, cpuStat);
-        }
-        for (VmMemoryStat memStat : memStats) {
-            correlator.add(1, memStat);
+            count++;
         }
     }
 
     void printUpdatedStatsImpl() {
         Iterator<TimeStampedPojoCorrelator.Correlation> iterator = correlator.iterator();
         while (iterator.hasNext()) {
-            printStats(numSpaces, table, iterator);
+            printStats(table, iterator);
         }
         table.render(out);
     }
+    
+    private static class DelegateInfo {
+        int colsPerDelegate;
+        long lastTimeStamp;
+    }
 
 }
--- a/client/cli/src/main/resources/com/redhat/thermostat/client/cli/strings.properties	Tue Jan 08 16:50:11 2013 -0500
+++ b/client/cli/src/main/resources/com/redhat/thermostat/client/cli/strings.properties	Wed Jan 09 14:59:30 2013 -0500
@@ -28,8 +28,6 @@
 COLUMN_HEADER_VM_NAME = VM_NAME
 COLUMN_HEADER_VM_STATUS = STATUS
 COLUMN_HEADER_TIME = TIME
-COLUMN_HEADER_CPU_PERCENT = %CPU
-COLUMN_HEADER_MEMORY_PATTERN = MEM.{0}
 
 VM_STOP_TIME_RUNNING = <Running>
 VM_STATUS_ALIVE = RUNNING
--- a/client/cli/src/test/java/com/redhat/thermostat/client/cli/internal/ActivatorTest.java	Tue Jan 08 16:50:11 2013 -0500
+++ b/client/cli/src/test/java/com/redhat/thermostat/client/cli/internal/ActivatorTest.java	Wed Jan 09 14:59:30 2013 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012 Red Hat, Inc.
+ * Copyright 2013 Red Hat, Inc.
  *
  * This file is part of Thermostat.
  *
@@ -58,10 +58,12 @@
 
     @Test
     public void testCommandsRegistered() throws Exception {
-        // Need to mock FrameworkUtil to avoid NPE in ShellCommand's no-arg constructor
+        // Need to mock FrameworkUtil to avoid NPE in ShellCommand and
+        // VMStatCommand's no-arg constructors
         PowerMockito.mockStatic(FrameworkUtil.class);
         Bundle mockBundle = mock(Bundle.class);
         when(FrameworkUtil.getBundle(ShellCommand.class)).thenReturn(mockBundle);
+        when(FrameworkUtil.getBundle(VMStatCommand.class)).thenReturn(mockBundle);
         StubBundleContext ctx = new StubBundleContext();
         when(mockBundle.getBundleContext()).thenReturn(ctx);
         
--- a/client/cli/src/test/java/com/redhat/thermostat/client/cli/internal/VmStatCommandTest.java	Tue Jan 08 16:50:11 2013 -0500
+++ b/client/cli/src/test/java/com/redhat/thermostat/client/cli/internal/VmStatCommandTest.java	Wed Jan 09 14:59:30 2013 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012 Red Hat, Inc.
+ * Copyright 2013 Red Hat, Inc.
  *
  * This file is part of Thermostat.
  *
@@ -40,11 +40,15 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyLong;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 import java.util.TimeZone;
@@ -59,26 +63,21 @@
 import org.junit.Ignore;
 import org.junit.Test;
 
+import com.redhat.thermostat.client.cli.VMStatPrintDelegate;
 import com.redhat.thermostat.common.ApplicationService;
 import com.redhat.thermostat.common.Timer;
 import com.redhat.thermostat.common.cli.CommandException;
 import com.redhat.thermostat.common.cli.SimpleArguments;
-import com.redhat.thermostat.common.dao.HostRef;
 import com.redhat.thermostat.common.dao.VmRef;
-import com.redhat.thermostat.common.utils.OSGIUtils;
-import com.redhat.thermostat.storage.model.VmCpuStat;
-import com.redhat.thermostat.storage.model.VmMemoryStat;
-import com.redhat.thermostat.storage.model.VmMemoryStat.Generation;
-import com.redhat.thermostat.storage.model.VmMemoryStat.Space;
+import com.redhat.thermostat.storage.model.TimeStampedPojo;
+import com.redhat.thermostat.test.StubBundleContext;
 import com.redhat.thermostat.test.TestCommandContextFactory;
 import com.redhat.thermostat.test.TestTimerFactory;
-import com.redhat.thermostat.vm.cpu.common.VmCpuStatDAO;
-import com.redhat.thermostat.vm.memory.common.VmMemoryStatDAO;
 
 public class VmStatCommandTest {
-
     private static Locale defaultLocale;
     private static TimeZone defaultTimeZone;
+    private static int NUM_ROWS = 3;
 
     @BeforeClass
     public static void setUpClass() {
@@ -94,34 +93,62 @@
         Locale.setDefault(defaultLocale);
     }
 
-    private VMStatCommand cmd;
-    private VmCpuStatDAO vmCpuStatDAO;
+    private VMStatPrintDelegate[] delegates;
     private TestCommandContextFactory cmdCtxFactory;
-    private VmMemoryStatDAO vmMemoryStatDAO;
     private TestTimerFactory timerFactory;
+    private ApplicationService appSvc;
 
     @Before
     public void setUp() {
+        delegates = new VMStatPrintDelegate[2];
+        final String[][] headers = {
+                { "FIRST", "SECOND", "THIRD" }, 
+                { "FOURTH", "FIFTH" } };
+        
+        final String[][][] rows = {
+                {
+                    { "1", "2", "3" },
+                    { "6", "7", "8" },
+                    { "11", "12", "13" },
+                },
+                {
+                    { "4", "5" },
+                    { "9", "10" },
+                    { "14", "15" }
+                }
+        };
         timerFactory = new TestTimerFactory();
-        ApplicationService appSvc = mock(ApplicationService.class);
+        appSvc = mock(ApplicationService.class);
         when(appSvc.getTimerFactory()).thenReturn(timerFactory);
         setupCommandContextFactory();
-
-        setupDAOs();
-
-        OSGIUtils serviceProvider = mock(OSGIUtils.class);
-        when(serviceProvider.getServiceAllowNull(VmCpuStatDAO.class)).thenReturn(vmCpuStatDAO);
-        when(serviceProvider.getServiceAllowNull(VmMemoryStatDAO.class)).thenReturn(vmMemoryStatDAO);
-        when(serviceProvider.getService(ApplicationService.class)).thenReturn(appSvc);
-
-        cmd = new VMStatCommand(serviceProvider);
+        
+        delegates[0] = mockDelegate(headers[0], rows[0]);
+        delegates[1] = mockDelegate(headers[1], rows[1]);
+    }
+    
+    private VMStatPrintDelegate mockDelegate(String[] headers, String[][] data) {
+        VMStatPrintDelegate delegate = mock(VMStatPrintDelegate.class);
+        List<TimeStampedPojo> stats = new ArrayList<>();
+        for (int i = 0; i < NUM_ROWS; i++) {
+            TimeStampedPojo stat = mock(TimeStampedPojo.class);
+            when(stat.getTimeStamp()).thenReturn(i * 1000L); // Increment by one second
+            stats.add(stat);
+        }
+        
+        // Need this syntax due to generics
+        doReturn(stats).when(delegate).getLatestStats(any(VmRef.class), eq(Long.MIN_VALUE));
+        when(delegate.getHeaders(stats.get(0))).thenReturn(Arrays.asList(headers));
+        for (int i = 0; i < data.length; i++) {
+            List<String> row = Arrays.asList(data[i]);
+            doReturn(row).when(delegate).getStatRow(eq(stats.get(i)));
+        }
+        
+        return delegate;
     }
 
     @After
     public void tearDown() {
-        vmCpuStatDAO = null;
         cmdCtxFactory = null;
-        cmd = null;
         timerFactory = null;
     }
 
@@ -129,116 +156,62 @@
         cmdCtxFactory = new TestCommandContextFactory();
     }
 
-    private void setupDAOs() {
-        vmCpuStatDAO = mock(VmCpuStatDAO.class);
-        int vmId = 234;
-        HostRef host = new HostRef("123", "dummy");
-        VmRef vm = new VmRef(host, 234, "dummy");
-        VmCpuStat cpustat1 = new VmCpuStat(2, vmId, 65);
-        VmCpuStat cpustat2 = new VmCpuStat(3, vmId, 70);
-        List<VmCpuStat> cpuStats = Arrays.asList(cpustat1, cpustat2);
-        List<VmCpuStat> cpuStats2 = Collections.emptyList();
-        when(vmCpuStatDAO.getLatestVmCpuStats(vm, Long.MIN_VALUE)).thenReturn(cpuStats).thenReturn(cpuStats2);
-
-        VmMemoryStat.Space space1_1_1 = newSpace("space1", 123456, 12345, 1, 0);
-        VmMemoryStat.Space space1_1_2 = newSpace("space2", 123456, 12345, 1, 0);
-        VmMemoryStat.Space[] spaces1_1 = new VmMemoryStat.Space[] { space1_1_1, space1_1_2 };
-        VmMemoryStat.Generation gen1_1 = newGeneration("gen1", "col1", 123456, 12345, spaces1_1);
-
-        VmMemoryStat.Space space1_2_1 = newSpace("space3", 123456, 12345, 1, 0);
-        VmMemoryStat.Space space1_2_2 = newSpace("space4", 123456, 12345, 1, 0);
-        VmMemoryStat.Space[] spaces1_2 = new VmMemoryStat.Space[] { space1_2_1, space1_2_2 };
-        VmMemoryStat.Generation gen1_2 = newGeneration("gen2", "col1", 123456, 12345, spaces1_2);
-
-        VmMemoryStat.Generation[] gens1 = new VmMemoryStat.Generation[] { gen1_1, gen1_2 };
-
-        VmMemoryStat memStat1 = new VmMemoryStat(1, vmId, gens1);
-
-        VmMemoryStat.Space space2_1_1 = newSpace("space1", 123456, 12345, 2, 0);
-        VmMemoryStat.Space space2_1_2 = newSpace("space2", 123456, 12345, 2, 0);
-        VmMemoryStat.Space[] spaces2_1 = new VmMemoryStat.Space[] { space2_1_1, space2_1_2 };
-        VmMemoryStat.Generation gen2_1 = newGeneration("gen1", "col1", 123456, 12345, spaces2_1);
-
-        VmMemoryStat.Space space2_2_1 = newSpace("space3", 123456, 12345, 3, 0);
-        VmMemoryStat.Space space2_2_2 = newSpace("space4", 123456, 12345, 4, 0);
-        VmMemoryStat.Space[] spaces2_2 = new VmMemoryStat.Space[] { space2_2_1, space2_2_2 };
-        VmMemoryStat.Generation gen2_2 = newGeneration("gen2", "col1", 123456, 12345, spaces2_2);
-
-        VmMemoryStat.Generation[] gens2 = new VmMemoryStat.Generation[] { gen2_1, gen2_2 };
-
-        VmMemoryStat memStat2 = new VmMemoryStat(2, vmId, gens2);
-
-        VmMemoryStat.Space space3_1_1 = newSpace("space1", 123456, 12345, 4, 0);
-        VmMemoryStat.Space space3_1_2 = newSpace("space2", 123456, 12345, 5, 0);
-        VmMemoryStat.Space[] spaces3_1 = new VmMemoryStat.Space[] { space3_1_1, space3_1_2 };
-        VmMemoryStat.Generation gen3_1 = newGeneration("gen1", "col1", 123456, 12345, spaces3_1);
-
-        VmMemoryStat.Space space3_2_1 = newSpace("space3", 123456, 12345, 6, 0);
-        VmMemoryStat.Space space3_2_2 = newSpace("space4", 123456, 12345, 7, 0);
-        VmMemoryStat.Space[] spaces3_2 = new VmMemoryStat.Space[] { space3_2_1, space3_2_2 };
-        VmMemoryStat.Generation gen3_2 = newGeneration("gen2", "col1", 123456, 12345, spaces3_2);
-
-        VmMemoryStat.Generation[] gens3 = new VmMemoryStat.Generation[] { gen3_1, gen3_2 };
-
-        VmMemoryStat memStat3 = new VmMemoryStat(3, vmId, gens3);
-
-        VmMemoryStat.Space space4_1_1 = newSpace("space1", 123456, 12345, 8, 0);
-        VmMemoryStat.Space space4_1_2 = newSpace("space2", 123456, 12345, 9, 0);
-        VmMemoryStat.Space[] spaces4_1 = new VmMemoryStat.Space[] { space4_1_1, space4_1_2 };
-        VmMemoryStat.Generation gen4_1 = newGeneration("gen4", "col1", 123456, 12345, spaces4_1);
-
-        VmMemoryStat.Space space4_2_1 = newSpace("space3", 123456, 12345, 10, 0);
-        VmMemoryStat.Space space4_2_2 = newSpace("space4", 123456, 12345, 11, 0);
-        VmMemoryStat.Space[] spaces4_2 = new VmMemoryStat.Space[] { space4_2_1, space4_2_2 };
-        VmMemoryStat.Generation gen4_2 = newGeneration("gen4", "col1", 123456, 12345, spaces4_2);
-
-        VmMemoryStat.Generation[] gens4 = new VmMemoryStat.Generation[] { gen4_1, gen4_2 };
-
-        VmMemoryStat memStat4 = new VmMemoryStat(4, vmId, gens4);
-
-        vmMemoryStatDAO = mock(VmMemoryStatDAO.class);
-        when(vmMemoryStatDAO.getLatestVmMemoryStats(vm, Long.MIN_VALUE))
-            .thenReturn(Arrays.asList(memStat1, memStat2, memStat3));
-
-        when(vmMemoryStatDAO.getLatestVmMemoryStats(vm, memStat3.getTimeStamp())).thenReturn(Arrays.asList(memStat4));
-
+    @Test
+    public void testOutput() throws CommandException {
+        StubBundleContext context = new StubBundleContext();
+        context.registerService(VMStatPrintDelegate.class.getName(), delegates[0], null);
+        context.registerService(VMStatPrintDelegate.class.getName(), delegates[1], null);
+        
+        VMStatCommand cmd = new VMStatCommand(context);
+        
+        SimpleArguments args = new SimpleArguments();
+        args.addArgument("vmId", "234");
+        args.addArgument("hostId", "123");
+        cmd.run(cmdCtxFactory.createContext(args));
+        String expected = "TIME        FIRST SECOND THIRD FOURTH FIFTH\n"
+                + "12:00:00 AM 1     2      3     4      5\n"
+                + "12:00:01 AM 6     7      8     9      10\n"
+                + "12:00:02 AM 11    12     13    14     15\n";
+        assertEquals(expected, cmdCtxFactory.getOutput());
     }
-
-    private Space newSpace(String name, long maxCapacity, long capacity, long used, int index) {
-        VmMemoryStat.Space space = new VmMemoryStat.Space();
-        space.setName(name);
-        space.setMaxCapacity(maxCapacity);
-        space.setCapacity(capacity);
-        space.setUsed(used);
-        space.setIndex(index);
-        return space;
-    }
-
-    private Generation newGeneration(String name, String collector, long maxCapacity, long capacity, Space[] spaces) {
-        VmMemoryStat.Generation gen = new VmMemoryStat.Generation();
-        gen.setName(name);
-        gen.setCollector(collector);
-        gen.setMaxCapacity(capacity);
-        gen.setSpaces(spaces);
-        return gen;
-    }
-
+    
     @Test
-    public void testBasicCPUMemory() throws CommandException {
+    public void testNoDelegates() throws CommandException {
+        StubBundleContext context = new StubBundleContext();
+        VMStatCommand cmd = new VMStatCommand(context);
+        
         SimpleArguments args = new SimpleArguments();
         args.addArgument("vmId", "234");
         args.addArgument("hostId", "123");
         cmd.run(cmdCtxFactory.createContext(args));
-        String expected = "TIME        %CPU MEM.space1 MEM.space2 MEM.space3 MEM.space4\n" +
-                          "12:00:00 AM      1 B        1 B        1 B        1 B\n" +
-                          "12:00:00 AM 65.0 2 B        2 B        3 B        4 B\n" +
-                          "12:00:00 AM 70.0 4 B        5 B        6 B        7 B\n";
+        String expected = "TIME\n";
         assertEquals(expected, cmdCtxFactory.getOutput());
-
     }
 
     @Test
     public void testContinuousMode() throws CommandException {
+        final String[][] data = {
+                { "16", "17", "18" },
+                { "19", "20" }
+        };
+        StubBundleContext context = new StubBundleContext();
+        context.registerService(ApplicationService.class.getName(), appSvc, null);
+        context.registerService(VMStatPrintDelegate.class.getName(), delegates[0], null);
+        context.registerService(VMStatPrintDelegate.class.getName(), delegates[1], null);
+        
+        // Add one more stat
+        TimeStampedPojo stat = mock(TimeStampedPojo.class);
+        // One second after previous timestamps
+        when(stat.getTimeStamp()).thenReturn(3000L);
+        List<TimeStampedPojo> stats = new ArrayList<>();
+        stats.add(stat);
+        
+        doReturn(stats).when(delegates[0]).getLatestStats(any(VmRef.class), eq(2000L));
+        doReturn(stats).when(delegates[1]).getLatestStats(any(VmRef.class), eq(2000L));
+        doReturn(Arrays.asList(data[0])).when(delegates[0]).getStatRow(eq(stat));
+        doReturn(Arrays.asList(data[1])).when(delegates[1]).getStatRow(eq(stat));
+        
+        final VMStatCommand cmd = new VMStatCommand(context);
         
         Thread t = new Thread() {
             public void run() {
@@ -261,10 +234,10 @@
             return;
         }
         assertTrue(timerFactory.isActive());
-        String expected = "TIME        %CPU MEM.space1 MEM.space2 MEM.space3 MEM.space4\n" +
-                          "12:00:00 AM      1 B        1 B        1 B        1 B\n" +
-                          "12:00:00 AM 65.0 2 B        2 B        3 B        4 B\n" +
-                          "12:00:00 AM 70.0 4 B        5 B        6 B        7 B\n";
+        String expected = "TIME        FIRST SECOND THIRD FOURTH FIFTH\n" +
+                "12:00:00 AM 1     2      3     4      5\n" +
+                "12:00:01 AM 6     7      8     9      10\n" +
+                "12:00:02 AM 11    12     13    14     15\n";
         assertEquals(expected, cmdCtxFactory.getOutput());
         assertEquals(1, timerFactory.getDelay());
         assertEquals(1, timerFactory.getInitialDelay());
@@ -273,11 +246,11 @@
 
         timerFactory.getAction().run();
 
-        expected = "TIME        %CPU MEM.space1 MEM.space2 MEM.space3 MEM.space4\n" +
-                   "12:00:00 AM      1 B        1 B        1 B        1 B\n" +
-                   "12:00:00 AM 65.0 2 B        2 B        3 B        4 B\n" +
-                   "12:00:00 AM 70.0 4 B        5 B        6 B        7 B\n" +
-                   "12:00:00 AM 70.0 8 B        9 B        10 B       11 B\n";
+        expected = "TIME        FIRST SECOND THIRD FOURTH FIFTH\n" +
+                "12:00:00 AM 1     2      3     4      5\n" +
+                "12:00:01 AM 6     7      8     9      10\n" +
+                "12:00:02 AM 11    12     13    14     15\n" +
+                "12:00:03 AM 16    17     18    19     20\n";
         assertEquals(expected, cmdCtxFactory.getOutput());
         cmdCtxFactory.setInput(" ");
         try {
@@ -290,18 +263,24 @@
 
     @Test
     public void testName() {
+        StubBundleContext context = new StubBundleContext();
+        VMStatCommand cmd = new VMStatCommand(context);
         assertEquals("vm-stat", cmd.getName());
     }
 
     @Test
     public void testDescAndUsage() {
-        assertNotNull(cmd.getUsage());
+        StubBundleContext context = new StubBundleContext();
+        VMStatCommand cmd = new VMStatCommand(context);
+        assertNotNull(cmd.getDescription());
         assertNotNull(cmd.getUsage());
     }
 
     @Ignore
     @Test
     public void testOptions() {
+        StubBundleContext context = new StubBundleContext();
+        VMStatCommand cmd = new VMStatCommand(context);
         Options options = cmd.getOptions();
         assertNotNull(options);
         assertEquals(3, options.getOptions().size());
@@ -325,9 +304,91 @@
         assertFalse(cont.isRequired());
         assertFalse(cont.hasArg());
     }
+    
+    @Test
+    public void testNoStats() throws CommandException {
+        // Fail stats != null check
+        VMStatPrintDelegate badDelegate = mock(VMStatPrintDelegate.class);
+        when(badDelegate.getLatestStats(any(VmRef.class), anyLong())).thenReturn(null);
+        
+        StubBundleContext context = new StubBundleContext();
+        context.registerService(VMStatPrintDelegate.class, delegates[0], null);
+        context.registerService(VMStatPrintDelegate.class, badDelegate, null);
+        context.registerService(VMStatPrintDelegate.class, delegates[1], null);
+        
+        VMStatCommand cmd = new VMStatCommand(context);
+        
+        SimpleArguments args = new SimpleArguments();
+        args.addArgument("vmId", "234");
+        args.addArgument("hostId", "123");
+        cmd.run(cmdCtxFactory.createContext(args));
+        String expected = "TIME        FIRST SECOND THIRD FOURTH FIFTH\n"
+                + "12:00:00 AM 1     2      3     4      5\n"
+                + "12:00:01 AM 6     7      8     9      10\n"
+                + "12:00:02 AM 11    12     13    14     15\n";
+        assertEquals(expected, cmdCtxFactory.getOutput());
+    }
+    
+    @Test
+    public void testNoHeaders() throws CommandException {
+        // Pass stats check, but fail headers check
+        VMStatPrintDelegate badDelegate = mock(VMStatPrintDelegate.class);
+        TimeStampedPojo stat = mock(TimeStampedPojo.class);
+        doReturn(Arrays.asList(stat)).when(badDelegate).getLatestStats(any(VmRef.class), anyLong());
+        when(badDelegate.getHeaders(any(TimeStampedPojo.class))).thenReturn(null);
+        
+        StubBundleContext context = new StubBundleContext();
+        context.registerService(VMStatPrintDelegate.class, delegates[0], null);
+        context.registerService(VMStatPrintDelegate.class, badDelegate, null);
+        context.registerService(VMStatPrintDelegate.class, delegates[1], null);
+        
+        VMStatCommand cmd = new VMStatCommand(context);
+        
+        SimpleArguments args = new SimpleArguments();
+        args.addArgument("vmId", "234");
+        args.addArgument("hostId", "123");
+        cmd.run(cmdCtxFactory.createContext(args));
+        String expected = "TIME        FIRST SECOND THIRD FOURTH FIFTH\n"
+                + "12:00:00 AM 1     2      3     4      5\n"
+                + "12:00:01 AM 6     7      8     9      10\n"
+                + "12:00:02 AM 11    12     13    14     15\n";
+        assertEquals(expected, cmdCtxFactory.getOutput());
+    }
+    
+    @Test
+    public void testUnevenStat() throws CommandException {
+        // Fewer stats than other delegates
+        VMStatPrintDelegate badDelegate = mock(VMStatPrintDelegate.class);
+        TimeStampedPojo stat1 = mock(TimeStampedPojo.class);
+        when(stat1.getTimeStamp()).thenReturn(1000L);
+        TimeStampedPojo stat2 = mock(TimeStampedPojo.class);
+        when(stat2.getTimeStamp()).thenReturn(2000L);
+        doReturn(Arrays.asList(stat1, stat2)).when(badDelegate).getLatestStats(any(VmRef.class), anyLong());
+        when(badDelegate.getHeaders(any(TimeStampedPojo.class))).thenReturn(Arrays.asList("BAD"));
+        when(badDelegate.getStatRow(any(TimeStampedPojo.class))).thenReturn(Arrays.asList("0"));
+        
+        StubBundleContext context = new StubBundleContext();
+        context.registerService(VMStatPrintDelegate.class, delegates[0], null);
+        context.registerService(VMStatPrintDelegate.class, badDelegate, null);
+        context.registerService(VMStatPrintDelegate.class, delegates[1], null);
+        
+        VMStatCommand cmd = new VMStatCommand(context);
+        
+        SimpleArguments args = new SimpleArguments();
+        args.addArgument("vmId", "234");
+        args.addArgument("hostId", "123");
+        cmd.run(cmdCtxFactory.createContext(args));
+        String expected = "TIME        FIRST SECOND THIRD BAD FOURTH FIFTH\n"
+                + "12:00:00 AM 1     2      3         4      5\n"
+                + "12:00:01 AM 6     7      8     0   9      10\n"
+                + "12:00:02 AM 11    12     13    0   14     15\n";
+        assertEquals(expected, cmdCtxFactory.getOutput());
+    }
 
     @Test
     public void testStorageRequired() {
+        StubBundleContext context = new StubBundleContext();
+        VMStatCommand cmd = new VMStatCommand(context);
         assertTrue(cmd.isStorageRequired());
     }
 }
--- a/common/core/src/main/java/com/redhat/thermostat/test/StubBundleContext.java	Tue Jan 08 16:50:11 2013 -0500
+++ b/common/core/src/main/java/com/redhat/thermostat/test/StubBundleContext.java	Wed Jan 09 14:59:30 2013 -0500
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012 Red Hat, Inc.
+ * Copyright 2013 Red Hat, Inc.
  *
  * This file is part of Thermostat.
  *
@@ -203,7 +203,13 @@
 
     @Override
     public ServiceReference getServiceReference(String clazz) {
-        throw new NotImplementedException();
+        ServiceReference result = null;
+        for (ServiceInformation info : registeredServices) {
+            if (info.serviceInterface.equals(clazz)) {
+                result = new StubServiceReference(info);
+            }
+        }
+        return result;
     }
 
     @Override
--- a/distribution/config/commands/vm-stat.properties	Tue Jan 08 16:50:11 2013 -0500
+++ b/distribution/config/commands/vm-stat.properties	Wed Jan 09 14:59:30 2013 -0500
@@ -1,6 +1,8 @@
 bundles = thermostat-client-cli-${project.version}.jar, \
           thermostat-vm-cpu-common-${project.version}.jar, \
+          thermostat-vm-cpu-client-cli-${project.version}.jar, \
           thermostat-vm-memory-common-${project.version}.jar, \
+          thermostat-vm-memory-client-cli-${project.version}.jar, \
           thermostat-storage-mongodb-${project.version}.jar, \
           thermostat-web-common-${project.version}.jar, \
           thermostat-web-client-${project.version}.jar, \
--- a/distribution/pom.xml	Tue Jan 08 16:50:11 2013 -0500
+++ b/distribution/pom.xml	Wed Jan 09 14:59:30 2013 -0500
@@ -412,6 +412,11 @@
     </dependency>
     <dependency>
       <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-vm-cpu-client-cli</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
       <artifactId>thermostat-vm-cpu-agent</artifactId>
       <version>${project.version}</version>
     </dependency>
@@ -447,6 +452,11 @@
     </dependency>
     <dependency>
       <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-vm-memory-client-cli</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
       <artifactId>thermostat-vm-memory-agent</artifactId>
       <version>${project.version}</version>
     </dependency>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-cpu/client-cli/pom.xml	Wed Jan 09 14:59:30 2013 -0500
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <artifactId>thermostat-vm-cpu</artifactId>
+    <groupId>com.redhat.thermostat</groupId>
+    <version>0.5.0-SNAPSHOT</version>
+  </parent>
+  <artifactId>thermostat-vm-cpu-client-cli</artifactId>
+  <packaging>bundle</packaging>
+  <name>Thermostat VM CPU CLI Client plugin</name>
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <extensions>true</extensions>
+        <configuration>
+          <instructions>
+            <Private-Package>
+                com.redhat.thermostat.vm.cpu.client.cli.internal
+            </Private-Package>
+            <Bundle-Vendor>Red Hat, Inc.</Bundle-Vendor>
+            <Bundle-Activator>com.redhat.thermostat.vm.cpu.client.cli.internal.Activator</Bundle-Activator>
+            <Bundle-SymbolicName>com.redhat.thermostat.vm.cpu.client.cli</Bundle-SymbolicName>
+            <!-- Do not autogenerate uses clauses in Manifests -->
+            <_nouses>true</_nouses>
+          </instructions>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+  <dependencies>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.osgi</groupId>
+      <artifactId>org.osgi.core</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.osgi</groupId>
+      <artifactId>org.osgi.compendium</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-common-core</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-vm-cpu-common</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-common-test</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-client-cli</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+  </dependencies>
+</project>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-cpu/client-cli/src/main/java/com/redhat/thermostat/vm/cpu/client/cli/internal/Activator.java	Wed Jan 09 14:59:30 2013 -0500
@@ -0,0 +1,80 @@
+/*
+ * 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.vm.cpu.client.cli.internal;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.util.tracker.ServiceTracker;
+
+import com.redhat.thermostat.client.cli.VMStatPrintDelegate;
+import com.redhat.thermostat.vm.cpu.common.VmCpuStatDAO;
+
+public class Activator implements BundleActivator {
+    
+    private ServiceTracker tracker;
+    private ServiceRegistration reg;
+
+    @Override
+    public void start(BundleContext context) throws Exception {
+        tracker = new ServiceTracker(context, VmCpuStatDAO.class.getName(), null) {
+            @Override
+            public Object addingService(ServiceReference reference) {
+                VmCpuStatDAO vmCpuStatDAO = (VmCpuStatDAO) super.addingService(reference);
+                VmCpuStatPrintDelegate delegate = new VmCpuStatPrintDelegate(vmCpuStatDAO);
+                reg = context.registerService(VMStatPrintDelegate.class.getName(), delegate, null);
+                return vmCpuStatDAO;
+            }
+            
+            @Override
+            public void removedService(ServiceReference reference,
+                    Object service) {
+                reg.unregister();
+                super.removedService(reference, service);
+            }
+        };
+        
+        tracker.open();
+    }
+
+    @Override
+    public void stop(BundleContext context) throws Exception {
+        tracker.close();
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-cpu/client-cli/src/main/java/com/redhat/thermostat/vm/cpu/client/cli/internal/LocaleResources.java	Wed Jan 09 14:59:30 2013 -0500
@@ -0,0 +1,51 @@
+/*
+ * 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.vm.cpu.client.cli.internal;
+
+import com.redhat.thermostat.common.locale.Translate;
+
+public enum LocaleResources {
+
+    COLUMN_HEADER_CPU_PERCENT,
+    ;
+
+    static final String RESOURCE_BUNDLE = "com.redhat.thermostat.vm.cpu.client.cli.strings";
+
+    public static Translate<LocaleResources> createLocalizer() {
+        return new Translate<>(RESOURCE_BUNDLE, LocaleResources.class);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-cpu/client-cli/src/main/java/com/redhat/thermostat/vm/cpu/client/cli/internal/VmCpuStatPrintDelegate.java	Wed Jan 09 14:59:30 2013 -0500
@@ -0,0 +1,86 @@
+/*
+ * 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.vm.cpu.client.cli.internal;
+
+import java.text.DecimalFormat;
+import java.util.Arrays;
+import java.util.List;
+
+import com.redhat.thermostat.client.cli.VMStatPrintDelegate;
+import com.redhat.thermostat.common.dao.VmRef;
+import com.redhat.thermostat.common.locale.Translate;
+import com.redhat.thermostat.storage.model.TimeStampedPojo;
+import com.redhat.thermostat.storage.model.VmCpuStat;
+import com.redhat.thermostat.vm.cpu.common.VmCpuStatDAO;
+
+public class VmCpuStatPrintDelegate implements VMStatPrintDelegate {
+    
+    private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
+    
+    private static final String CPU_PERCENT = translator.localize(LocaleResources.COLUMN_HEADER_CPU_PERCENT);
+    
+    private VmCpuStatDAO cpuStatDAO;
+
+    public VmCpuStatPrintDelegate(VmCpuStatDAO cpuStatDAO) {
+        this.cpuStatDAO = cpuStatDAO;
+    }
+
+    @Override
+    public List<? extends TimeStampedPojo> getLatestStats(VmRef ref,
+            long timestamp) {
+        return cpuStatDAO.getLatestVmCpuStats(ref, timestamp);
+    }
+
+    @Override
+    public List<String> getHeaders(TimeStampedPojo stat) {
+        return Arrays.asList(CPU_PERCENT);
+    }
+
+    @Override
+    public List<String> getStatRow(TimeStampedPojo stat) {
+        VmCpuStat cpuStat = (VmCpuStat) stat;
+        DecimalFormat format = new DecimalFormat("#0.0");
+        String cpuLoad = cpuStat != null ? format.format(cpuStat.getCpuLoad()) : "";
+        return Arrays.asList(cpuLoad);
+    }
+
+    @Override
+    public int getOrderValue() {
+        return ORDER_CPU_GROUP;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-cpu/client-cli/src/main/resources/com/redhat/thermostat/vm/cpu/client/cli/strings.properties	Wed Jan 09 14:59:30 2013 -0500
@@ -0,0 +1,1 @@
+COLUMN_HEADER_CPU_PERCENT = %CPU
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-cpu/client-cli/src/test/java/com/redhat/thermostat/vm/cpu/client/cli/internal/ActivatorTest.java	Wed Jan 09 14:59:30 2013 -0500
@@ -0,0 +1,84 @@
+/*
+ * 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.vm.cpu.client.cli.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import org.junit.Test;
+
+import com.redhat.thermostat.client.cli.VMStatPrintDelegate;
+import com.redhat.thermostat.test.StubBundleContext;
+import com.redhat.thermostat.vm.cpu.common.VmCpuStatDAO;
+
+public class ActivatorTest {
+    
+    @Test
+    public void verifyActivatorDoesNotRegisterServiceOnMissingDeps() throws Exception {
+        StubBundleContext context = new StubBundleContext();
+
+        Activator activator = new Activator();
+
+        activator.start(context);
+
+        assertEquals(0, context.getAllServices().size());
+        assertEquals(1, context.getServiceListeners().size());
+
+        activator.stop(context);
+    }
+
+    @Test
+    public void verifyActivatorRegistersServices() throws Exception {
+        StubBundleContext context = new StubBundleContext();
+        VmCpuStatDAO dao = mock(VmCpuStatDAO.class);
+
+        context.registerService(VmCpuStatDAO.class, dao, null);
+
+        Activator activator = new Activator();
+
+        activator.start(context);
+
+        assertTrue(context.isServiceRegistered(VMStatPrintDelegate.class.getName(), VmCpuStatPrintDelegate.class));
+
+        activator.stop(context);
+
+        assertEquals(0, context.getServiceListeners().size());
+        assertEquals(1, context.getAllServices().size());
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-cpu/client-cli/src/test/java/com/redhat/thermostat/vm/cpu/client/cli/internal/LocaleResourcesTest.java	Wed Jan 09 14:59:30 2013 -0500
@@ -0,0 +1,53 @@
+/*
+ * 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.vm.cpu.client.cli.internal;
+
+import com.redhat.thermostat.test.locale.AbstractLocaleResourcesTest;
+
+public class LocaleResourcesTest extends AbstractLocaleResourcesTest<LocaleResources> {
+
+    @Override
+    protected Class<LocaleResources> getEnumClass() {
+        return LocaleResources.class;
+    }
+
+    @Override
+    protected String getResourceBundle() {
+        return LocaleResources.RESOURCE_BUNDLE;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-cpu/client-cli/src/test/java/com/redhat/thermostat/vm/cpu/client/cli/internal/VmCpuStatPrintDelegateTest.java	Wed Jan 09 14:59:30 2013 -0500
@@ -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.vm.cpu.client.cli.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.common.dao.HostRef;
+import com.redhat.thermostat.common.dao.VmRef;
+import com.redhat.thermostat.storage.model.TimeStampedPojo;
+import com.redhat.thermostat.storage.model.VmCpuStat;
+import com.redhat.thermostat.vm.cpu.common.VmCpuStatDAO;
+
+public class VmCpuStatPrintDelegateTest {
+
+    private VmCpuStatDAO vmCpuStatDAO;
+    private VmCpuStatPrintDelegate delegate;
+    private VmRef vm;
+    private List<VmCpuStat> cpuStats;
+
+    @Before
+    public void setUp() {
+        setupDAOs();
+        delegate = new VmCpuStatPrintDelegate(vmCpuStatDAO);
+    }
+
+    @After
+    public void tearDown() {
+        vmCpuStatDAO = null;
+    }
+
+    private void setupDAOs() {
+        vmCpuStatDAO = mock(VmCpuStatDAO.class);
+        int vmId = 234;
+        HostRef host = new HostRef("123", "dummy");
+        vm = new VmRef(host, 234, "dummy");
+        VmCpuStat cpustat1 = new VmCpuStat(2, vmId, 65);
+        VmCpuStat cpustat2 = new VmCpuStat(3, vmId, 70);
+        cpuStats = Arrays.asList(cpustat1, cpustat2);
+        when(vmCpuStatDAO.getLatestVmCpuStats(vm, Long.MIN_VALUE)).thenReturn(cpuStats);
+    }
+    
+    @Test
+    public void testGetLatestStats() {
+        List<? extends TimeStampedPojo> stats = delegate.getLatestStats(vm, Long.MIN_VALUE);
+        assertEquals(cpuStats, stats);
+    }
+    
+    @Test
+    public void testGetHeaders() {
+        List<String> headers = delegate.getHeaders(cpuStats.get(0));
+        assertEquals(Arrays.asList("%CPU"), headers);
+    }
+
+    @Test
+    public void testGetStatRow() throws CommandException {
+        final List<String> row1 = Arrays.asList("65.0");
+        final List<String> row2 = Arrays.asList("70.0");
+        assertEquals(row1, delegate.getStatRow(cpuStats.get(0)));
+        assertEquals(row2, delegate.getStatRow(cpuStats.get(1)));
+    }
+
+}
--- a/vm-cpu/pom.xml	Tue Jan 08 16:50:11 2013 -0500
+++ b/vm-cpu/pom.xml	Wed Jan 09 14:59:30 2013 -0500
@@ -52,6 +52,7 @@
 
   <modules>
     <module>agent</module>
+    <module>client-cli</module>
     <module>client-core</module>
     <module>client-swing</module>
     <module>common</module>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-memory/client-cli/pom.xml	Wed Jan 09 14:59:30 2013 -0500
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <artifactId>thermostat-vm-memory</artifactId>
+    <groupId>com.redhat.thermostat</groupId>
+    <version>0.5.0-SNAPSHOT</version>
+  </parent>
+  <artifactId>thermostat-vm-memory-client-cli</artifactId>
+  <packaging>bundle</packaging>
+  <name>Thermostat VM Memory CLI Client plugin</name>
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <extensions>true</extensions>
+        <configuration>
+          <instructions>
+            <Private-Package>
+                com.redhat.thermostat.vm.memory.client.cli.internal
+            </Private-Package>
+            <Bundle-Vendor>Red Hat, Inc.</Bundle-Vendor>
+            <Bundle-Activator>com.redhat.thermostat.vm.memory.client.cli.internal.Activator</Bundle-Activator>
+            <Bundle-SymbolicName>com.redhat.thermostat.vm.memory.client.cli</Bundle-SymbolicName>
+            <!-- Do not autogenerate uses clauses in Manifests -->
+            <_nouses>true</_nouses>
+          </instructions>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+  <dependencies>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.osgi</groupId>
+      <artifactId>org.osgi.core</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.osgi</groupId>
+      <artifactId>org.osgi.compendium</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-common-core</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-vm-memory-common</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-common-test</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-client-cli</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+  </dependencies>
+</project>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-memory/client-cli/src/main/java/com/redhat/thermostat/vm/memory/client/cli/internal/Activator.java	Wed Jan 09 14:59:30 2013 -0500
@@ -0,0 +1,80 @@
+/*
+ * 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.vm.memory.client.cli.internal;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.util.tracker.ServiceTracker;
+
+import com.redhat.thermostat.client.cli.VMStatPrintDelegate;
+import com.redhat.thermostat.vm.memory.common.VmMemoryStatDAO;
+
+public class Activator implements BundleActivator {
+    
+    private ServiceTracker tracker;
+    private ServiceRegistration reg;
+
+    @Override
+    public void start(BundleContext context) throws Exception {
+        tracker = new ServiceTracker(context, VmMemoryStatDAO.class.getName(), null) {
+            @Override
+            public Object addingService(ServiceReference reference) {
+                VmMemoryStatDAO vmMemoryStatDAO = (VmMemoryStatDAO) super.addingService(reference);
+                VmMemoryStatPrintDelegate delegate = new VmMemoryStatPrintDelegate(vmMemoryStatDAO);
+                reg = context.registerService(VMStatPrintDelegate.class.getName(), delegate, null);
+                return vmMemoryStatDAO;
+            }
+            
+            @Override
+            public void removedService(ServiceReference reference,
+                    Object service) {
+                reg.unregister();
+                super.removedService(reference, service);
+            }
+        };
+        
+        tracker.open();
+    }
+
+    @Override
+    public void stop(BundleContext context) throws Exception {
+        tracker.close();
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-memory/client-cli/src/main/java/com/redhat/thermostat/vm/memory/client/cli/internal/LocaleResources.java	Wed Jan 09 14:59:30 2013 -0500
@@ -0,0 +1,53 @@
+/*
+ * 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.vm.memory.client.cli.internal;
+
+import com.redhat.thermostat.common.locale.Translate;
+
+public enum LocaleResources {
+
+    VALUE_AND_UNIT,
+
+    COLUMN_HEADER_MEMORY_PATTERN,
+    ;
+
+    static final String RESOURCE_BUNDLE = "com.redhat.thermostat.vm.memory.client.cli.strings";
+
+    public static Translate<LocaleResources> createLocalizer() {
+        return new Translate<>(RESOURCE_BUNDLE, LocaleResources.class);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-memory/client-cli/src/main/java/com/redhat/thermostat/vm/memory/client/cli/internal/VmMemoryStatPrintDelegate.java	Wed Jan 09 14:59:30 2013 -0500
@@ -0,0 +1,101 @@
+/*
+ * 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.vm.memory.client.cli.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.redhat.thermostat.client.cli.VMStatPrintDelegate;
+import com.redhat.thermostat.common.Size;
+import com.redhat.thermostat.common.dao.VmRef;
+import com.redhat.thermostat.common.locale.Translate;
+import com.redhat.thermostat.storage.model.TimeStampedPojo;
+import com.redhat.thermostat.storage.model.VmMemoryStat;
+import com.redhat.thermostat.vm.memory.common.VmMemoryStatDAO;
+
+public class VmMemoryStatPrintDelegate implements VMStatPrintDelegate {
+    
+    private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
+    
+    private VmMemoryStatDAO memoryStatDAO;
+    
+    public VmMemoryStatPrintDelegate(VmMemoryStatDAO memoryStatDAO) {
+        this.memoryStatDAO = memoryStatDAO;
+    }
+
+    @Override
+    public List<? extends TimeStampedPojo> getLatestStats(VmRef ref, long timestamp) {
+        return memoryStatDAO.getLatestVmMemoryStats(ref, timestamp);
+    }
+
+    @Override
+    public List<String> getHeaders(TimeStampedPojo stat) {
+        return getSpacesNames(stat);
+    }
+    
+    @Override
+    public List<String> getStatRow(TimeStampedPojo stat) {
+        return getMemoryUsage(stat);
+    }
+
+    private List<String> getSpacesNames(TimeStampedPojo stat) {
+        List<String> spacesNames = new ArrayList<>();
+        VmMemoryStat memStat = (VmMemoryStat) stat;
+        for (VmMemoryStat.Generation gen : memStat.getGenerations()) {
+            for (VmMemoryStat.Space space : gen.getSpaces()) {
+                spacesNames.add(translator.localize(LocaleResources.COLUMN_HEADER_MEMORY_PATTERN, space.getName()));
+            }
+        }
+        return spacesNames;
+    }
+
+    private List<String> getMemoryUsage(TimeStampedPojo stat) {
+        List<String> memoryUsage = new ArrayList<>();
+        for (VmMemoryStat.Generation gen : ((VmMemoryStat) stat).getGenerations()) {
+            for (VmMemoryStat.Space space : gen.getSpaces()) {
+                memoryUsage.add(Size.bytes(space.getUsed()).toString());
+            }
+        }
+        return memoryUsage;
+    }
+
+    @Override
+    public int getOrderValue() {
+        return ORDER_MEMORY_GROUP;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-memory/client-cli/src/main/resources/com/redhat/thermostat/vm/memory/client/cli/strings.properties	Wed Jan 09 14:59:30 2013 -0500
@@ -0,0 +1,3 @@
+VALUE_AND_UNIT = {0} {1}
+
+COLUMN_HEADER_MEMORY_PATTERN = MEM.{0}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-memory/client-cli/src/test/java/com/redhat/thermostat/vm/memory/client/cli/internal/ActivatorTest.java	Wed Jan 09 14:59:30 2013 -0500
@@ -0,0 +1,84 @@
+/*
+ * 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.vm.memory.client.cli.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import org.junit.Test;
+
+import com.redhat.thermostat.client.cli.VMStatPrintDelegate;
+import com.redhat.thermostat.test.StubBundleContext;
+import com.redhat.thermostat.vm.memory.common.VmMemoryStatDAO;
+
+public class ActivatorTest {
+    
+    @Test
+    public void verifyActivatorDoesNotRegisterServiceOnMissingDeps() throws Exception {
+        StubBundleContext context = new StubBundleContext();
+
+        Activator activator = new Activator();
+
+        activator.start(context);
+
+        assertEquals(0, context.getAllServices().size());
+        assertEquals(1, context.getServiceListeners().size());
+
+        activator.stop(context);
+    }
+
+    @Test
+    public void verifyActivatorRegistersServices() throws Exception {
+        StubBundleContext context = new StubBundleContext();
+        VmMemoryStatDAO dao = mock(VmMemoryStatDAO.class);
+
+        context.registerService(VmMemoryStatDAO.class, dao, null);
+
+        Activator activator = new Activator();
+
+        activator.start(context);
+
+        assertTrue(context.isServiceRegistered(VMStatPrintDelegate.class.getName(), VmMemoryStatPrintDelegate.class));
+
+        activator.stop(context);
+
+        assertEquals(0, context.getServiceListeners().size());
+        assertEquals(1, context.getAllServices().size());
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-memory/client-cli/src/test/java/com/redhat/thermostat/vm/memory/client/cli/internal/LocaleResourcesTest.java	Wed Jan 09 14:59:30 2013 -0500
@@ -0,0 +1,53 @@
+/*
+ * 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.vm.memory.client.cli.internal;
+
+import com.redhat.thermostat.test.locale.AbstractLocaleResourcesTest;
+
+public class LocaleResourcesTest extends AbstractLocaleResourcesTest<LocaleResources> {
+
+    @Override
+    protected Class<LocaleResources> getEnumClass() {
+        return LocaleResources.class;
+    }
+
+    @Override
+    protected String getResourceBundle() {
+        return LocaleResources.RESOURCE_BUNDLE;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-memory/client-cli/src/test/java/com/redhat/thermostat/vm/memory/client/cli/internal/VmMemoryStatPrintDelegateTest.java	Wed Jan 09 14:59:30 2013 -0500
@@ -0,0 +1,172 @@
+/*
+ * 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.vm.memory.client.cli.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.redhat.thermostat.client.cli.VMStatPrintDelegate;
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.common.dao.HostRef;
+import com.redhat.thermostat.common.dao.VmRef;
+import com.redhat.thermostat.storage.model.TimeStampedPojo;
+import com.redhat.thermostat.storage.model.VmMemoryStat;
+import com.redhat.thermostat.storage.model.VmMemoryStat.Generation;
+import com.redhat.thermostat.storage.model.VmMemoryStat.Space;
+import com.redhat.thermostat.vm.memory.common.VmMemoryStatDAO;
+
+public class VmMemoryStatPrintDelegateTest {
+
+    private VmMemoryStatDAO vmMemoryStatDAO;
+    private VMStatPrintDelegate delegate;
+    private VmRef vm;
+    private List<VmMemoryStat> memoryStats;
+
+    @Before
+    public void setUp() {
+        setupDAOs();
+        delegate = new VmMemoryStatPrintDelegate(vmMemoryStatDAO);
+    }
+
+    @After
+    public void tearDown() {
+        vmMemoryStatDAO = null;
+    }
+
+    private void setupDAOs() {
+        int vmId = 234;
+        HostRef host = new HostRef("123", "dummy");
+        vm = new VmRef(host, 234, "dummy");
+
+        VmMemoryStat.Space space1_1_1 = newSpace("space1", 123456, 12345, 1, 0);
+        VmMemoryStat.Space space1_1_2 = newSpace("space2", 123456, 12345, 1, 0);
+        VmMemoryStat.Space[] spaces1_1 = new VmMemoryStat.Space[] { space1_1_1, space1_1_2 };
+        VmMemoryStat.Generation gen1_1 = newGeneration("gen1", "col1", 123456, 12345, spaces1_1);
+
+        VmMemoryStat.Space space1_2_1 = newSpace("space3", 123456, 12345, 1, 0);
+        VmMemoryStat.Space space1_2_2 = newSpace("space4", 123456, 12345, 1, 0);
+        VmMemoryStat.Space[] spaces1_2 = new VmMemoryStat.Space[] { space1_2_1, space1_2_2 };
+        VmMemoryStat.Generation gen1_2 = newGeneration("gen2", "col1", 123456, 12345, spaces1_2);
+
+        VmMemoryStat.Generation[] gens1 = new VmMemoryStat.Generation[] { gen1_1, gen1_2 };
+
+        VmMemoryStat memStat1 = new VmMemoryStat(1, vmId, gens1);
+
+        VmMemoryStat.Space space2_1_1 = newSpace("space1", 123456, 12345, 2, 0);
+        VmMemoryStat.Space space2_1_2 = newSpace("space2", 123456, 12345, 2, 0);
+        VmMemoryStat.Space[] spaces2_1 = new VmMemoryStat.Space[] { space2_1_1, space2_1_2 };
+        VmMemoryStat.Generation gen2_1 = newGeneration("gen1", "col1", 123456, 12345, spaces2_1);
+
+        VmMemoryStat.Space space2_2_1 = newSpace("space3", 123456, 12345, 3, 0);
+        VmMemoryStat.Space space2_2_2 = newSpace("space4", 123456, 12345, 4, 0);
+        VmMemoryStat.Space[] spaces2_2 = new VmMemoryStat.Space[] { space2_2_1, space2_2_2 };
+        VmMemoryStat.Generation gen2_2 = newGeneration("gen2", "col1", 123456, 12345, spaces2_2);
+
+        VmMemoryStat.Generation[] gens2 = new VmMemoryStat.Generation[] { gen2_1, gen2_2 };
+
+        VmMemoryStat memStat2 = new VmMemoryStat(2, vmId, gens2);
+
+        VmMemoryStat.Space space3_1_1 = newSpace("space1", 123456, 12345, 4, 0);
+        VmMemoryStat.Space space3_1_2 = newSpace("space2", 123456, 12345, 5, 0);
+        VmMemoryStat.Space[] spaces3_1 = new VmMemoryStat.Space[] { space3_1_1, space3_1_2 };
+        VmMemoryStat.Generation gen3_1 = newGeneration("gen1", "col1", 123456, 12345, spaces3_1);
+
+        VmMemoryStat.Space space3_2_1 = newSpace("space3", 123456, 12345, 6, 0);
+        VmMemoryStat.Space space3_2_2 = newSpace("space4", 123456, 12345, 7, 0);
+        VmMemoryStat.Space[] spaces3_2 = new VmMemoryStat.Space[] { space3_2_1, space3_2_2 };
+        VmMemoryStat.Generation gen3_2 = newGeneration("gen2", "col1", 123456, 12345, spaces3_2);
+
+        VmMemoryStat.Generation[] gens3 = new VmMemoryStat.Generation[] { gen3_1, gen3_2 };
+
+        VmMemoryStat memStat3 = new VmMemoryStat(3, vmId, gens3);
+
+        vmMemoryStatDAO = mock(VmMemoryStatDAO.class);
+        memoryStats = Arrays.asList(memStat1, memStat2, memStat3);
+        when(vmMemoryStatDAO.getLatestVmMemoryStats(vm, Long.MIN_VALUE))
+            .thenReturn(memoryStats);
+    }
+
+    private Space newSpace(String name, long maxCapacity, long capacity, long used, int index) {
+        VmMemoryStat.Space space = new VmMemoryStat.Space();
+        space.setName(name);
+        space.setMaxCapacity(maxCapacity);
+        space.setCapacity(capacity);
+        space.setUsed(used);
+        space.setIndex(index);
+        return space;
+    }
+
+    private Generation newGeneration(String name, String collector, long maxCapacity, long capacity, Space[] spaces) {
+        VmMemoryStat.Generation gen = new VmMemoryStat.Generation();
+        gen.setName(name);
+        gen.setCollector(collector);
+        gen.setMaxCapacity(capacity);
+        gen.setSpaces(spaces);
+        return gen;
+    }
+
+    @Test
+    public void testGetLatestStats() {
+        List<? extends TimeStampedPojo> stats = delegate.getLatestStats(vm, Long.MIN_VALUE);
+        assertEquals(memoryStats, stats);
+    }
+    
+    @Test
+    public void testGetHeaders() {
+        List<String> headers = delegate.getHeaders(memoryStats.get(0));
+        assertEquals(Arrays.asList("MEM.space1", "MEM.space2", "MEM.space3", "MEM.space4"), headers);
+    }
+
+    @Test
+    public void testGetStatRow() throws CommandException {
+        final List<String> row1 = Arrays.asList("1 B", "1 B", "1 B", "1 B");
+        final List<String> row2 = Arrays.asList("2 B", "2 B", "3 B", "4 B");
+        final List<String> row3 = Arrays.asList("4 B", "5 B", "6 B", "7 B");
+        assertEquals(row1, delegate.getStatRow(memoryStats.get(0)));
+        assertEquals(row2, delegate.getStatRow(memoryStats.get(1)));
+        assertEquals(row3, delegate.getStatRow(memoryStats.get(2)));
+    }
+
+}
--- a/vm-memory/pom.xml	Tue Jan 08 16:50:11 2013 -0500
+++ b/vm-memory/pom.xml	Wed Jan 09 14:59:30 2013 -0500
@@ -52,6 +52,7 @@
 
   <modules>
     <module>agent</module>
+    <module>client-cli</module>
     <module>client-core</module>
     <module>client-swing</module>
     <module>common</module>