changeset 909:2930e0c260cc

Merge
author Elliott Baron <ebaron@redhat.com>
date Mon, 14 Jan 2013 14:04:50 -0500
parents 1e51015e63e5 (diff) b1b81446c892 (current diff)
children 421d8a954893
files agent/cli/src/main/java/com/redhat/thermostat/agent/cli/db/DBConfig.java agent/cli/src/main/java/com/redhat/thermostat/agent/cli/db/DBOptionParser.java agent/cli/src/main/java/com/redhat/thermostat/agent/cli/db/DBStartupConfiguration.java agent/cli/src/main/java/com/redhat/thermostat/agent/cli/db/MongoProcessRunner.java agent/cli/src/main/java/com/redhat/thermostat/agent/cli/db/StalePidFileException.java agent/cli/src/main/java/com/redhat/thermostat/agent/cli/db/StorageAlreadyRunningException.java agent/cli/src/main/java/com/redhat/thermostat/agent/cli/db/StorageStartException.java agent/cli/src/main/java/com/redhat/thermostat/agent/cli/db/StorageStopException.java agent/cli/src/main/java/com/redhat/thermostat/agent/cli/impl/StorageCommand.java agent/cli/src/test/java/com/redhat/thermostat/agent/cli/impl/StorageCommandTest.java bundles/pom.xml bundles/src/main/java/com/redhat/thermostat/bundles/OSGiRegistry.java bundles/src/main/java/com/redhat/thermostat/bundles/impl/Activator.java bundles/src/main/java/com/redhat/thermostat/bundles/impl/BundleLoader.java bundles/src/main/java/com/redhat/thermostat/bundles/impl/OSGiRegistryImpl.java bundles/src/test/java/com/redhat/thermostat/bundles/OSGiRegistryTest.java bundles/src/test/java/com/redhat/thermostat/bundles/impl/BundleLoaderTest.java bundles/src/test/java/com/redhat/thermostat/bundles/impl/OSGiRegistryImplTest.java common/core/src/main/java/com/redhat/thermostat/test/MockQuery.java distribution/pom.xml storage/core/src/main/java/com/redhat/thermostat/storage/model/AgentIdPojo.java web/common/src/test/java/com/redhat/thermostat/web/common/WebQueryTest.java
diffstat 34 files changed, 1781 insertions(+), 309 deletions(-) [+]
line wrap: on
line diff
--- a/client/cli/pom.xml	Thu Jan 10 16:16:29 2013 +0100
+++ b/client/cli/pom.xml	Mon Jan 14 14:04:50 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	Mon Jan 14 14:04:50 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	Thu Jan 10 16:16:29 2013 +0100
+++ b/client/cli/src/main/java/com/redhat/thermostat/client/cli/internal/LocaleResources.java	Mon Jan 14 14:04:50 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	Thu Jan 10 16:16:29 2013 +0100
+++ b/client/cli/src/main/java/com/redhat/thermostat/client/cli/internal/VMStatCommand.java	Mon Jan 14 14:04:50 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	Thu Jan 10 16:16:29 2013 +0100
+++ b/client/cli/src/main/java/com/redhat/thermostat/client/cli/internal/VMStatPrinter.java	Mon Jan 14 14:04:50 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	Thu Jan 10 16:16:29 2013 +0100
+++ b/client/cli/src/main/resources/com/redhat/thermostat/client/cli/strings.properties	Mon Jan 14 14:04:50 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	Thu Jan 10 16:16:29 2013 +0100
+++ b/client/cli/src/test/java/com/redhat/thermostat/client/cli/internal/ActivatorTest.java	Mon Jan 14 14:04:50 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	Thu Jan 10 16:16:29 2013 +0100
+++ b/client/cli/src/test/java/com/redhat/thermostat/client/cli/internal/VmStatCommandTest.java	Mon Jan 14 14:04:50 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	Thu Jan 10 16:16:29 2013 +0100
+++ b/common/core/src/main/java/com/redhat/thermostat/test/StubBundleContext.java	Mon Jan 14 14:04:50 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/common/test/src/main/java/com/redhat/thermostat/test/locale/AbstractLocaleResourcesTest.java	Thu Jan 10 16:16:29 2013 +0100
+++ b/common/test/src/main/java/com/redhat/thermostat/test/locale/AbstractLocaleResourcesTest.java	Mon Jan 14 14:04:50 2013 -0500
@@ -1,3 +1,39 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
 package com.redhat.thermostat.test.locale;
 
 import java.io.IOException;
@@ -31,4 +67,4 @@
     
     protected abstract String getResourceBundle();
 
-}
\ No newline at end of file
+}
--- a/distribution/config/commands/vm-stat.properties	Thu Jan 10 16:16:29 2013 +0100
+++ b/distribution/config/commands/vm-stat.properties	Mon Jan 14 14:04:50 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	Thu Jan 10 16:16:29 2013 +0100
+++ b/distribution/pom.xml	Mon Jan 14 14:04:50 2013 -0500
@@ -407,6 +407,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>
@@ -442,6 +447,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>
--- a/storage/core/src/main/java/com/redhat/thermostat/storage/core/StorageProviderUtil.java	Thu Jan 10 16:16:29 2013 +0100
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/core/StorageProviderUtil.java	Mon Jan 14 14:04:50 2013 -0500
@@ -1,3 +1,39 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
 package com.redhat.thermostat.storage.core;
 
 import org.osgi.framework.Bundle;
--- a/storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/Activator.java	Thu Jan 10 16:16:29 2013 +0100
+++ b/storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/Activator.java	Mon Jan 14 14:04:50 2013 -0500
@@ -1,3 +1,39 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
 package com.redhat.thermostat.storage.mongodb.internal;
 
 import org.osgi.framework.BundleActivator;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-cpu/client-cli/pom.xml	Mon Jan 14 14:04:50 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	Mon Jan 14 14:04:50 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	Mon Jan 14 14:04:50 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	Mon Jan 14 14:04:50 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	Mon Jan 14 14:04:50 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	Mon Jan 14 14:04:50 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	Mon Jan 14 14:04:50 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	Mon Jan 14 14:04:50 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	Thu Jan 10 16:16:29 2013 +0100
+++ b/vm-cpu/pom.xml	Mon Jan 14 14:04:50 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	Mon Jan 14 14:04:50 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	Mon Jan 14 14:04:50 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	Mon Jan 14 14:04:50 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	Mon Jan 14 14:04:50 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	Mon Jan 14 14:04:50 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	Mon Jan 14 14:04:50 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	Mon Jan 14 14:04:50 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	Mon Jan 14 14:04:50 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	Thu Jan 10 16:16:29 2013 +0100
+++ b/vm-memory/pom.xml	Mon Jan 14 14:04:50 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>
--- a/web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorageProvider.java	Thu Jan 10 16:16:29 2013 +0100
+++ b/web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorageProvider.java	Mon Jan 14 14:04:50 2013 -0500
@@ -1,3 +1,39 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
 package com.redhat.thermostat.web.client.internal;
 
 import com.redhat.thermostat.storage.config.AuthenticationConfiguration;
--- a/web/common/src/test/java/com/redhat/thermostat/web/common/WebQueryTest.java	Thu Jan 10 16:16:29 2013 +0100
+++ b/web/common/src/test/java/com/redhat/thermostat/web/common/WebQueryTest.java	Mon Jan 14 14:04:50 2013 -0500
@@ -1,17 +1,3 @@
-package com.redhat.thermostat.web.common;
-import static org.junit.Assert.assertEquals;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import org.junit.Test;
-
-import com.redhat.thermostat.storage.core.Category;
-import com.redhat.thermostat.storage.core.Key;
-import com.redhat.thermostat.storage.core.Query.Criteria;
-import com.redhat.thermostat.storage.model.Pojo;
-
 /*
  * Copyright 2012 Red Hat, Inc.
  *
@@ -48,6 +34,20 @@
  * to do so, delete this exception statement from your version.
  */
 
+package com.redhat.thermostat.web.common;
+import static org.junit.Assert.assertEquals;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Test;
+
+import com.redhat.thermostat.storage.core.Category;
+import com.redhat.thermostat.storage.core.Key;
+import com.redhat.thermostat.storage.core.Query.Criteria;
+import com.redhat.thermostat.storage.model.Pojo;
+
 public class WebQueryTest {
 
     private static class TestObj implements Pojo {