changeset 2533:3ab518b2f8c5

Refactor notes commands collection into subcommand structure Reviewed-by: jerboaa Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2016-October/021310.html Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2016-November/021540.html
author Andrew Azores <aazores@redhat.com>
date Fri, 14 Oct 2016 15:28:23 -0400
parents c8e331460b1e
children f623de4ce913
files integration-tests/itest-run/src/test/java/com/redhat/thermostat/itest/NotesCommandsTest.java notes/client-cli/pom.xml notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/AbstractNotesCommand.java notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/Activator.java notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/AddNoteCommand.java notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/AddNoteSubcommand.java notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/DeleteNoteCommand.java notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/DeleteNoteSubcommand.java notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/ListNotesCommand.java notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/ListNotesSubcommand.java notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/NoteIdCompleterService.java notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/NoteIdsFinder.java notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/NotesCommand.java notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/NotesControlCommand.java notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/NotesSubcommand.java notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/UpdateNoteCommand.java notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/UpdateNoteSubcommand.java notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/locale/LocaleResources.java notes/client-cli/src/main/resources/com/redhat/thermostat/notes/client/cli/locale/strings.properties notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/AbstractNotesCommandTest.java notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/ActivatorTest.java notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/AddNoteCommandTest.java notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/AddNoteSubcommandTest.java notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/DeleteNoteCommandTest.java notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/DeleteNoteSubcommandTest.java notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/ListNotesCommandTest.java notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/ListNotesSubcommandTest.java notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/NoteIdCompleterServiceTest.java notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/NoteIdsFinderTest.java notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/NotesControlCommandTest.java notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/UpdateNoteCommandTest.java notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/UpdateNoteSubcommandTest.java notes/distribution/thermostat-plugin.xml
diffstat 33 files changed, 2174 insertions(+), 2346 deletions(-) [+]
line wrap: on
line diff
--- a/integration-tests/itest-run/src/test/java/com/redhat/thermostat/itest/NotesCommandsTest.java	Thu Nov 17 12:47:48 2016 -0500
+++ b/integration-tests/itest-run/src/test/java/com/redhat/thermostat/itest/NotesCommandsTest.java	Fri Oct 14 15:28:23 2016 -0400
@@ -69,6 +69,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
+import static org.junit.matchers.JUnitMatchers.containsString;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -115,19 +116,18 @@
         mongoStorage.getConnection().disconnect();
     }
 
-    @Test
-    public void testAddNoteWithStandardOptions() throws Exception {
-        doAddNoteTest(Arrays.asList("-c", "this is the note content"), "this is the note content");
+    private static void addAgentInfo(Add<AgentInformation> add, AgentInformation pojo) {
+        add.set(Key.AGENT_ID.getName(), pojo.getAgentId());
+        add.set(AgentInfoDAO.ALIVE_KEY.getName(), pojo.isAlive());
+        add.set(AgentInfoDAO.START_TIME_KEY.getName(), pojo.getStartTime());
+        add.set(AgentInfoDAO.STOP_TIME_KEY.getName(), pojo.getStopTime());
+        add.set(AgentInfoDAO.CONFIG_LISTEN_ADDRESS.getName(), pojo.getConfigListenAddress());
+        add.apply();
     }
 
     @Test
-    public void testAddNoteWithNoContentFlag() throws Exception {
-        doAddNoteTest(Arrays.asList("this", "is", "content"), "this is content");
-    }
-
-    @Test
-    public void testAddNoteWithDoubleDashArgDelimiter() throws Exception {
-        doAddNoteTest(Arrays.asList("--", "--fakeArg", "more"), "--fakeArg more");
+    public void testAddNoteWithStandardOptions() throws Exception {
+        doAddNoteTest(Arrays.asList("-c", "this is the note content"), "this is the note content");
     }
 
     @Test
@@ -147,15 +147,16 @@
         List<String> args = new ArrayList<>();
         args.add("-a");
         args.add("foo-agentid");
+        args.add("add");
         args.addAll(contentArgs);
-        Spawn cmd = commandAgainstMongo("add-note", args.toArray(new String[args.size()]));
+        Spawn cmd = commandAgainstMongo("notes", args.toArray(new String[args.size()]));
         handleAuthPrompt(cmd, "mongodb://127.0.0.1:27518", USERNAME, PASSWORD);
         cmd.expectClose();
 
-        assertCommandIsFound(cmd.getCurrentStandardOutContents(), cmd.getCurrentStandardErrContents());
-        assertNoExceptions(cmd.getCurrentStandardOutContents(), cmd.getCurrentStandardErrContents());
+        assertCommandIsFound(cmd);
+        assertNoExceptions(cmd);
         if (expectedResult == null) {
-            assertCouldNotParseUnrecognizedOptions(cmd.getCurrentStandardOutContents(), cmd.getCurrentStandardErrContents());
+            assertCouldNotParseUnrecognizedOptions(cmd);
             return;
         }
 
@@ -177,6 +178,18 @@
         mongoStorage2.getConnection().disconnect();
     }
 
+    private static void assertCouldNotParseUnrecognizedOptions(Spawn spawn) {
+        assertCouldNotParseUnrecognizedOptions(spawn.getCurrentStandardOutContents(), spawn.getCurrentStandardErrContents());
+    }
+
+    private static void assertCouldNotParseUnrecognizedOptions(String stdout, String stderr) {
+        String message = "Could not parse options: Unrecognized option:";
+        boolean outContainsMessage = stdout.contains(message);
+        boolean errContainsMessage = stderr.contains(message);
+        assertTrue("stdout or stderr should have contained \"" + message + "\"",
+                outContainsMessage || errContainsMessage);
+    }
+
     private static BackingStorage getAndConnectStorage(Connection.ConnectionListener listener) {
         final String url = "mongodb://127.0.0.1:27518";
         StorageCredentials creds = new StorageCredentials() {
@@ -203,13 +216,59 @@
         return storage;
     }
 
-    private static void addAgentInfo(Add<AgentInformation> add, AgentInformation pojo) {
-        add.set(Key.AGENT_ID.getName(), pojo.getAgentId());
-        add.set(AgentInfoDAO.ALIVE_KEY.getName(), pojo.isAlive());
-        add.set(AgentInfoDAO.START_TIME_KEY.getName(), pojo.getStartTime());
-        add.set(AgentInfoDAO.STOP_TIME_KEY.getName(), pojo.getStopTime());
-        add.set(AgentInfoDAO.CONFIG_LISTEN_ADDRESS.getName(), pojo.getConfigListenAddress());
-        add.apply();
+    @Test
+    public void testFailsForUnknownSubcommand() throws Exception {
+        Spawn cmd = runSubcommand("lsit", "-a", "foo-agentid"); // intentional typo: lsit, rather than list
+
+        assertCommandIsFound(cmd);
+        assertNoExceptions(cmd);
+        assertThat(cmd.getCurrentStandardErrContents(), containsString("The subcommand \"lsit\" is not recognized"));
+    }
+
+    @Test
+    public void testAddNoteSubcommandRegistered() throws Exception {
+        Spawn cmd = runSubcommand("add");
+
+        assertCommandIsFound(cmd);
+        assertNoExceptions(cmd);
+        assertThat(cmd.getCurrentStandardOutContents(), containsString("Missing required option: -c"));
+    }
+
+    @Test
+    public void testDeleteNoteSubcommandRegistered() throws Exception {
+        Spawn cmd = runSubcommand("delete");
+
+        assertCommandIsFound(cmd);
+        assertNoExceptions(cmd);
+        assertThat(cmd.getCurrentStandardOutContents(), containsString("Missing required option: -n"));
+    }
+
+    @Test
+    public void testListNotesSubcommandRegistered() throws Exception {
+        Spawn cmd = runSubcommand("list");
+
+        assertCommandIsFound(cmd);
+        assertNoExceptions(cmd);
+        assertThat(cmd.getCurrentStandardErrContents(), containsString("A Host or VM ID must be provided"));
+    }
+
+    @Test
+    public void testUpdateNoteSubcommandRegistered() throws Exception {
+        Spawn cmd = runSubcommand("update");
+
+        assertCommandIsFound(cmd);
+        assertNoExceptions(cmd);
+        assertThat(cmd.getCurrentStandardOutContents(), containsString("Missing required options: -c, -n"));
+    }
+
+    private static Spawn runSubcommand(String subcommand, String... extraArgs) throws Exception {
+        List<String> args = new ArrayList<>();
+        args.add(subcommand);
+        args.addAll(Arrays.asList(extraArgs));
+        Spawn cmd = commandAgainstMongo("notes", args.toArray(new String[args.size()]));
+        handleAuthPrompt(cmd, "mongodb://127.0.0.1:27518", USERNAME, PASSWORD);
+        cmd.expectClose();
+        return cmd;
     }
 
     private static Spawn commandAgainstMongo(String cmd, String... args) throws IOException {
@@ -224,14 +283,6 @@
         return spawnThermostat(true, completeArgs.toArray(new String[0]));
     }
 
-    private static void assertCouldNotParseUnrecognizedOptions(String stdout, String stderr) {
-        String message = "Could not parse options: Unrecognized option:";
-        boolean outContainsMessage = stdout.contains(message);
-        boolean errContainsMessage = stderr.contains(message);
-        assertTrue("stdout or stderr should have contained \"" + message + "\"",
-                outContainsMessage || errContainsMessage);
-    }
-
     private static class CountdownConnectionListener implements Connection.ConnectionListener {
 
         private final Connection.ConnectionStatus target;
--- a/notes/client-cli/pom.xml	Thu Nov 17 12:47:48 2016 -0500
+++ b/notes/client-cli/pom.xml	Fri Oct 14 15:28:23 2016 -0400
@@ -56,14 +56,11 @@
           <instructions>
             <Bundle-Vendor>Red Hat, Inc.</Bundle-Vendor>
             <Bundle-SymbolicName>com.redhat.thermostat.notes.client.cli</Bundle-SymbolicName>
-            <Export-Package>
-               com.redhat.thermostat.notes.client.cli,
-               com.redhat.thermostat.notes.client.cli.locale
-            </Export-Package>
+            <Bundle-Activator>com.redhat.thermostat.notes.client.cli.internal.Activator</Bundle-Activator>
             <Private-Package>
-               com.redhat.thermostat.notes.client.cli.internal
+              com.redhat.thermostat.notes.client.cli.internal,
+              com.redhat.thermostat.notes.client.cli.locale
             </Private-Package>
-            <Bundle-Activator>com.redhat.thermostat.notes.client.cli.internal.Activator</Bundle-Activator>
             <!-- Do not autogenerate uses clauses in Manifests -->
             <_nouses>true</_nouses>
           </instructions>
--- a/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/AbstractNotesCommand.java	Thu Nov 17 12:47:48 2016 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,178 +0,0 @@
-/*
- * Copyright 2012-2016 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.notes.client.cli.internal;
-
-import com.redhat.thermostat.client.cli.AgentArgument;
-import com.redhat.thermostat.client.cli.VmArgument;
-import com.redhat.thermostat.common.cli.AbstractCommand;
-import com.redhat.thermostat.common.cli.Arguments;
-import com.redhat.thermostat.common.cli.CommandException;
-import com.redhat.thermostat.common.cli.DependencyServices;
-import com.redhat.thermostat.notes.client.cli.locale.LocaleResources;
-import com.redhat.thermostat.notes.common.HostNoteDAO;
-import com.redhat.thermostat.notes.common.VmNoteDAO;
-import com.redhat.thermostat.shared.locale.Translate;
-import com.redhat.thermostat.storage.core.AgentId;
-import com.redhat.thermostat.storage.core.HostRef;
-import com.redhat.thermostat.storage.core.VmId;
-import com.redhat.thermostat.storage.core.VmRef;
-import com.redhat.thermostat.storage.dao.AgentInfoDAO;
-import com.redhat.thermostat.storage.dao.HostInfoDAO;
-import com.redhat.thermostat.storage.dao.VmInfoDAO;
-import com.redhat.thermostat.storage.model.AgentInformation;
-import com.redhat.thermostat.storage.model.VmInfo;
-
-import java.util.Objects;
-
-public abstract class AbstractNotesCommand extends AbstractCommand implements NotesCommand {
-
-    protected static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
-
-    protected DependencyServices dependencyServices;
-
-    protected VmInfoDAO vmInfoDAO;
-    protected HostInfoDAO hostInfoDAO;
-    protected AgentInfoDAO agentInfoDAO;
-    protected VmNoteDAO vmNoteDAO;
-    protected HostNoteDAO hostNoteDAO;
-
-    public AbstractNotesCommand() {
-        this.dependencyServices = new DependencyServices();
-    }
-
-    protected void setupServices() throws CommandException {
-        vmInfoDAO = dependencyServices.getRequiredService(VmInfoDAO.class);
-        hostInfoDAO = dependencyServices.getRequiredService(HostInfoDAO.class);
-        agentInfoDAO = dependencyServices.getRequiredService(AgentInfoDAO.class);
-        vmNoteDAO = dependencyServices.getRequiredService(VmNoteDAO.class);
-        hostNoteDAO = dependencyServices.getRequiredService(HostNoteDAO.class);
-    }
-
-    protected static void assertExpectedAgentAndVmArgsProvided(Arguments args) throws CommandException {
-        boolean hasVmArg = args.hasArgument(VmArgument.ARGUMENT_NAME);
-        boolean hasAgentArg = args.hasArgument(AgentArgument.ARGUMENT_NAME);
-        if (hasVmArg && hasAgentArg) {
-            throw new CommandException(translator.localize(LocaleResources.HOST_AND_VM_ARGS_PROVIDED));
-        } else if (!(hasVmArg || hasAgentArg)) {
-            throw new CommandException(translator.localize(LocaleResources.NO_ARGS_PROVIDED));
-        }
-    }
-
-    protected void checkVmExists(VmId vmId) throws CommandException {
-        Objects.requireNonNull(vmId);
-        VmInfo info = vmInfoDAO.getVmInfo(vmId);
-        requireNonNull(info, translator.localize(LocaleResources.INVALID_VMID, vmId.get()));
-    }
-
-    protected void checkAgentExists(AgentId agentId) throws CommandException {
-        Objects.requireNonNull(agentId);
-        AgentInformation info = agentInfoDAO.getAgentInformation(agentId);
-        requireNonNull(info, translator.localize(LocaleResources.INVALID_AGENTID, agentId.get()));
-    }
-
-    protected VmRef getVmRefFromVmId(VmId vmId) {
-        VmInfo vmInfo = vmInfoDAO.getVmInfo(vmId);
-        return new VmRef(getHostRefFromAgentId(new AgentId(vmInfo.getAgentId())), vmId.get(), vmInfo.getVmPid(), vmInfo.getVmName());
-    }
-
-    protected HostRef getHostRefFromAgentId(AgentId agentId) {
-        String hostName = hostInfoDAO.getHostInfo(agentId).getHostname();
-        return new HostRef(agentId.get(), hostName);
-    }
-
-    protected static String getNoteId(Arguments args) throws CommandException {
-        if (!args.hasArgument(NOTE_ID_ARGUMENT)) {
-            throw new CommandException(translator.localize(LocaleResources.NOTE_ID_ARG_REQUIRED));
-        }
-        return args.getArgument(NOTE_ID_ARGUMENT);
-    }
-
-    protected static String getNoteContent(Arguments args) throws CommandException {
-        if (args.hasArgument(NOTE_CONTENT_ARGUMENT)) {
-            String content = args.getArgument(NOTE_CONTENT_ARGUMENT);
-            if (content == null) {
-                content = "";
-            }
-            return content;
-        } else { // assume all non-option arguments are together meant to be the note content
-            StringBuilder sb = new StringBuilder();
-            for (String word : args.getNonOptionArguments()) {
-                sb.append(word).append(' ');
-            }
-            String content = sb.toString().trim();
-            if (content.isEmpty()) {
-                throw new CommandException(translator.localize(LocaleResources.NOTE_CONTENT_ARG_REQUIRED));
-            }
-            return content;
-        }
-    }
-
-    @Override
-    public void setVmInfoDao(VmInfoDAO vmInfoDao) {
-        dependencyServices.addService(VmInfoDAO.class, vmInfoDao);
-    }
-
-    @Override
-    public void setHostInfoDao(HostInfoDAO hostInfoDao) {
-        dependencyServices.addService(HostInfoDAO.class, hostInfoDao);
-    }
-
-    @Override
-    public void setAgentInfoDao(AgentInfoDAO agentInfoDao) {
-        dependencyServices.addService(AgentInfoDAO.class, agentInfoDao);
-    }
-
-    @Override
-    public void setVmNoteDao(VmNoteDAO vmNoteDAO) {
-        dependencyServices.addService(VmNoteDAO.class, vmNoteDAO);
-    }
-
-    @Override
-    public void setHostNoteDao(HostNoteDAO hostNoteDAO) {
-        dependencyServices.addService(HostNoteDAO.class, hostNoteDAO);
-    }
-
-    @Override
-    public void servicesUnavailable() {
-        dependencyServices.removeService(VmInfoDAO.class);
-        dependencyServices.removeService(HostInfoDAO.class);
-        dependencyServices.removeService(AgentInfoDAO.class);
-        dependencyServices.removeService(VmNoteDAO.class);
-        dependencyServices.removeService(HostNoteDAO.class);
-    }
-
-}
--- a/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/Activator.java	Thu Nov 17 12:47:48 2016 -0500
+++ b/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/Activator.java	Fri Oct 14 15:28:23 2016 -0400
@@ -39,8 +39,6 @@
 import com.redhat.thermostat.common.MultipleServiceTracker;
 import com.redhat.thermostat.common.MultipleServiceTracker.DependencyProvider;
 import com.redhat.thermostat.common.cli.Command;
-import com.redhat.thermostat.common.cli.CommandRegistry;
-import com.redhat.thermostat.common.cli.CommandRegistryImpl;
 import com.redhat.thermostat.common.cli.CompleterService;
 import com.redhat.thermostat.notes.common.HostNoteDAO;
 import com.redhat.thermostat.notes.common.VmNoteDAO;
@@ -49,31 +47,30 @@
 import com.redhat.thermostat.storage.dao.VmInfoDAO;
 import org.osgi.framework.BundleActivator;
 import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
 
 import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.Hashtable;
 import java.util.List;
-import java.util.Objects;
 
 public class Activator implements BundleActivator {
 
-    private CommandRegistry reg;
+    private ServiceRegistration commandRegistration;
+    private ServiceRegistration completerServiceRegistration;
     private MultipleServiceTracker serviceTracker;
-    private MultipleServiceTracker noteIdCompleterDepsTracker;
 
     @Override
     public void start(final BundleContext context) throws Exception {
-        reg = new CommandRegistryImpl(context);
+        final ListNotesSubcommand listNotesSubcommand = new ListNotesSubcommand();
+        final AddNoteSubcommand addNoteSubcommand = new AddNoteSubcommand();
+        final DeleteNoteSubcommand deleteNoteSubcommand = new DeleteNoteSubcommand();
+        final UpdateNoteSubcommand updateNoteSubcommand = new UpdateNoteSubcommand();
 
-        ListNotesCommand listNotesCommand = new ListNotesCommand();
-        registerCommand(ListNotesCommand.NAME, listNotesCommand);
-        AddNoteCommand addNoteCommand = new AddNoteCommand();
-        registerCommand(AddNoteCommand.NAME, addNoteCommand);
-        DeleteNoteCommand deleteNoteCommand = new DeleteNoteCommand();
-        registerCommand(DeleteNoteCommand.NAME, deleteNoteCommand);
-        UpdateNoteCommand updateNoteCommand = new UpdateNoteCommand();
-        registerCommand(UpdateNoteCommand.NAME, updateNoteCommand);
+        final NoteIdsFinder noteIdsFinder = new NoteIdsFinder();
 
-        final List<AbstractNotesCommand> commands = Arrays.asList(listNotesCommand, addNoteCommand, deleteNoteCommand, updateNoteCommand);
+        final NotesControlCommand controlCommand = new NotesControlCommand(noteIdsFinder, addNoteSubcommand,
+                deleteNoteSubcommand, updateNoteSubcommand, listNotesSubcommand);
 
         Class<?>[] serviceDeps = new Class<?>[]{
                 VmInfoDAO.class,
@@ -84,6 +81,10 @@
         };
 
         serviceTracker = new MultipleServiceTracker(context, serviceDeps, new MultipleServiceTracker.Action() {
+
+            private final List<NotesSubcommand> subcommands =
+                    Arrays.asList(listNotesSubcommand, addNoteSubcommand, deleteNoteSubcommand, updateNoteSubcommand);
+
             @Override
             public void dependenciesAvailable(DependencyProvider services) {
                 VmInfoDAO vmInfoDAO = services.get(VmInfoDAO.class);
@@ -92,49 +93,32 @@
                 VmNoteDAO vmNoteDAO = services.get(VmNoteDAO.class);
                 HostNoteDAO hostNoteDAO = services.get(HostNoteDAO.class);
 
-                for (NotesCommand command : commands) {
-                    command.setVmInfoDao(vmInfoDAO);
-                    command.setHostInfoDao(hostInfoDAO);
-                    command.setAgentInfoDao(agentInfoDAO);
-                    command.setVmNoteDao(vmNoteDAO);
-                    command.setHostNoteDao(hostNoteDAO);
+                for (NotesSubcommand subcommand : subcommands) {
+                    subcommand.bindVmInfoDao(vmInfoDAO);
+                    subcommand.bindHostInfoDao(hostInfoDAO);
+                    subcommand.bindAgentInfoDao(agentInfoDAO);
+                    subcommand.bindVmNoteDao(vmNoteDAO);
+                    subcommand.bindHostNoteDao(hostNoteDAO);
                 }
+
+                noteIdsFinder.bindVmNoteDao(vmNoteDAO);
+                noteIdsFinder.bindHostNoteDao(hostNoteDAO);
             }
 
             @Override
             public void dependenciesUnavailable() {
-                for (NotesCommand command : commands) {
+                noteIdsFinder.servicesUnavailable();
+                for (NotesSubcommand command : subcommands) {
                     command.servicesUnavailable();
                 }
             }
         });
-
         serviceTracker.open();
 
-        final NoteIdCompleterService noteIdCompleterService = new NoteIdCompleterService();
-        final Class<?>[] noteIdCompleterDeps = new Class<?>[] { HostNoteDAO.class, VmNoteDAO.class };
-        noteIdCompleterDepsTracker = new MultipleServiceTracker(context, noteIdCompleterDeps, new MultipleServiceTracker.Action() {
-            @Override
-            public void dependenciesAvailable(DependencyProvider services) {
-                HostNoteDAO hostNoteDAO = services.get(HostNoteDAO.class);
-                VmNoteDAO vmNoteDAO = services.get(VmNoteDAO.class);
-                noteIdCompleterService.setHostNoteDao(hostNoteDAO);
-                noteIdCompleterService.setVmNoteDao(vmNoteDAO);
-            }
-
-            @Override
-            public void dependenciesUnavailable() {
-                noteIdCompleterService.setHostNoteDao(null);
-                noteIdCompleterService.setVmNoteDao(null);
-            }
-        });
-        noteIdCompleterDepsTracker.open();
-
-        context.registerService(CompleterService.class, noteIdCompleterService, null);
-    }
-
-    private void registerCommand(String name, Command command) {
-        reg.registerCommand(name, command);
+        Dictionary<String, String> properties = new Hashtable<>();
+        properties.put(Command.NAME, NotesControlCommand.COMMAND_NAME);
+        commandRegistration = context.registerService(Command.class.getName(), controlCommand, properties);
+        completerServiceRegistration = context.registerService(CompleterService.class, controlCommand, properties);
     }
 
     @Override
@@ -142,11 +126,11 @@
         if (serviceTracker != null) {
             serviceTracker.close();
         }
-        if (noteIdCompleterDepsTracker != null) {
-            noteIdCompleterDepsTracker.close();
+        if (commandRegistration != null) {
+            commandRegistration.unregister();
         }
-        if (reg != null) {
-            reg.unregisterCommands();
+        if (completerServiceRegistration != null) {
+            completerServiceRegistration.unregister();
         }
     }
 
--- a/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/AddNoteCommand.java	Thu Nov 17 12:47:48 2016 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,90 +0,0 @@
-/*
- * Copyright 2012-2016 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.notes.client.cli.internal;
-
-import com.redhat.thermostat.client.cli.AgentArgument;
-import com.redhat.thermostat.client.cli.VmArgument;
-import com.redhat.thermostat.common.cli.Arguments;
-import com.redhat.thermostat.common.cli.CommandContext;
-import com.redhat.thermostat.common.cli.CommandException;
-import com.redhat.thermostat.notes.common.HostNote;
-import com.redhat.thermostat.notes.common.VmNote;
-import com.redhat.thermostat.storage.core.AgentId;
-import com.redhat.thermostat.storage.core.VmId;
-
-import java.util.UUID;
-
-public class AddNoteCommand extends AbstractNotesCommand {
-
-    static final String NAME = "add-note";
-
-    @Override
-    public void run(CommandContext ctx) throws CommandException {
-        setupServices();
-
-        Arguments args = ctx.getArguments();
-        assertExpectedAgentAndVmArgsProvided(args);
-
-        String noteContent = getNoteContent(args);
-
-        if (args.hasArgument(VmArgument.ARGUMENT_NAME)) {
-            VmId vmId = VmArgument.required(args).getVmId();
-            checkVmExists(vmId);
-            String agentId = vmInfoDAO.getVmInfo(vmId).getAgentId();
-            checkAgentExists(new AgentId(agentId));
-            VmNote note = new VmNote();
-            note.setId(UUID.randomUUID().toString());
-            note.setVmId(vmId.get());
-            note.setAgentId(agentId);
-            note.setContent(noteContent);
-            note.setTimeStamp(System.currentTimeMillis());
-
-            vmNoteDAO.add(note);
-        } else {
-            AgentId agentId = AgentArgument.required(args).getAgentId();
-            checkAgentExists(agentId);
-            HostNote note = new HostNote();
-            note.setId(UUID.randomUUID().toString());
-            note.setAgentId(agentId.get());
-            note.setContent(noteContent);
-            note.setTimeStamp(System.currentTimeMillis());
-
-            hostNoteDAO.add(note);
-        }
-    }
-
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/AddNoteSubcommand.java	Fri Oct 14 15:28:23 2016 -0400
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2012-2016 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.notes.client.cli.internal;
+
+import com.redhat.thermostat.client.cli.AgentArgument;
+import com.redhat.thermostat.client.cli.VmArgument;
+import com.redhat.thermostat.common.cli.Arguments;
+import com.redhat.thermostat.common.cli.CommandContext;
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.notes.common.HostNote;
+import com.redhat.thermostat.notes.common.HostNoteDAO;
+import com.redhat.thermostat.notes.common.VmNote;
+import com.redhat.thermostat.notes.common.VmNoteDAO;
+import com.redhat.thermostat.storage.core.AgentId;
+import com.redhat.thermostat.storage.core.VmId;
+import com.redhat.thermostat.storage.dao.VmInfoDAO;
+
+import java.util.UUID;
+
+public class AddNoteSubcommand extends NotesSubcommand {
+
+    static final String SUBCOMMAND_NAME = "add";
+
+    @Override
+    public void run(CommandContext ctx) throws CommandException {
+        Arguments args = ctx.getArguments();
+        assertExpectedAgentAndVmArgsProvided(args);
+
+        String noteContent = getNoteContent(args);
+
+        if (args.hasArgument(VmArgument.ARGUMENT_NAME)) {
+            VmInfoDAO vmInfoDAO = services.getRequiredService(VmInfoDAO.class);
+            VmNoteDAO vmNoteDAO = services.getRequiredService(VmNoteDAO.class);
+
+            VmId vmId = VmArgument.required(args).getVmId();
+            checkVmExists(vmId);
+            String agentId = vmInfoDAO.getVmInfo(vmId).getAgentId();
+            checkAgentExists(new AgentId(agentId));
+            VmNote note = new VmNote();
+            note.setId(UUID.randomUUID().toString());
+            note.setVmId(vmId.get());
+            note.setAgentId(agentId);
+            note.setContent(noteContent);
+            note.setTimeStamp(System.currentTimeMillis());
+
+            vmNoteDAO.add(note);
+        } else {
+            HostNoteDAO hostNoteDAO = services.getRequiredService(HostNoteDAO.class);
+
+            AgentId agentId = AgentArgument.required(args).getAgentId();
+            checkAgentExists(agentId);
+            HostNote note = new HostNote();
+            note.setId(UUID.randomUUID().toString());
+            note.setAgentId(agentId.get());
+            note.setContent(noteContent);
+            note.setTimeStamp(System.currentTimeMillis());
+
+            hostNoteDAO.add(note);
+        }
+    }
+
+}
--- a/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/DeleteNoteCommand.java	Thu Nov 17 12:47:48 2016 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,73 +0,0 @@
-/*
- * Copyright 2012-2016 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.notes.client.cli.internal;
-
-import com.redhat.thermostat.client.cli.AgentArgument;
-import com.redhat.thermostat.client.cli.VmArgument;
-import com.redhat.thermostat.common.cli.Arguments;
-import com.redhat.thermostat.common.cli.CommandContext;
-import com.redhat.thermostat.common.cli.CommandException;
-import com.redhat.thermostat.storage.core.AgentId;
-import com.redhat.thermostat.storage.core.VmId;
-
-public class DeleteNoteCommand extends AbstractNotesCommand {
-
-    static final String NAME = "delete-note";
-
-    @Override
-    public void run(CommandContext ctx) throws CommandException {
-        setupServices();
-
-        Arguments args = ctx.getArguments();
-        assertExpectedAgentAndVmArgsProvided(args);
-
-        String noteId = getNoteId(args);
-
-        if (args.hasArgument(VmArgument.ARGUMENT_NAME)) {
-            VmId vmId = VmArgument.required(args).getVmId();
-            checkVmExists(vmId);
-            AgentId agentId = new AgentId(vmInfoDAO.getVmInfo(vmId).getAgentId());
-            checkAgentExists(agentId);
-            vmNoteDAO.removeById(getVmRefFromVmId(vmId), noteId);
-        } else {
-            AgentId agentId = AgentArgument.required(args).getAgentId();
-            checkAgentExists(agentId);
-            hostNoteDAO.removeById(getHostRefFromAgentId(agentId), noteId);
-        }
-    }
-
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/DeleteNoteSubcommand.java	Fri Oct 14 15:28:23 2016 -0400
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2012-2016 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.notes.client.cli.internal;
+
+import com.redhat.thermostat.client.cli.AgentArgument;
+import com.redhat.thermostat.client.cli.VmArgument;
+import com.redhat.thermostat.common.cli.Arguments;
+import com.redhat.thermostat.common.cli.CommandContext;
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.notes.common.HostNoteDAO;
+import com.redhat.thermostat.notes.common.VmNoteDAO;
+import com.redhat.thermostat.storage.core.AgentId;
+import com.redhat.thermostat.storage.core.VmId;
+import com.redhat.thermostat.storage.dao.VmInfoDAO;
+
+public class DeleteNoteSubcommand extends NotesSubcommand {
+
+    static final String SUBCOMMAND_NAME = "delete";
+
+    @Override
+    public void run(CommandContext ctx) throws CommandException {
+        Arguments args = ctx.getArguments();
+        assertExpectedAgentAndVmArgsProvided(args);
+
+        String noteId = getNoteId(args);
+
+        if (args.hasArgument(VmArgument.ARGUMENT_NAME)) {
+            VmNoteDAO vmNoteDao = services.getRequiredService(VmNoteDAO.class);
+            VmInfoDAO vmInfoDao = services.getRequiredService(VmInfoDAO.class);
+
+            VmId vmId = VmArgument.required(args).getVmId();
+            checkVmExists(vmId);
+            AgentId agentId = new AgentId(vmInfoDao.getVmInfo(vmId).getAgentId());
+            checkAgentExists(agentId);
+            vmNoteDao.removeById(getVmRefFromVmId(vmId), noteId);
+        } else {
+            HostNoteDAO hostNoteDao = services.getRequiredService(HostNoteDAO.class);
+
+            AgentId agentId = AgentArgument.required(args).getAgentId();
+            checkAgentExists(agentId);
+            hostNoteDao.removeById(getHostRefFromAgentId(agentId), noteId);
+        }
+    }
+
+}
--- a/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/ListNotesCommand.java	Thu Nov 17 12:47:48 2016 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,189 +0,0 @@
-/*
- * Copyright 2012-2016 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.notes.client.cli.internal;
-
-import com.redhat.thermostat.client.cli.AgentArgument;
-import com.redhat.thermostat.client.cli.VmArgument;
-import com.redhat.thermostat.common.Clock;
-import com.redhat.thermostat.common.cli.Arguments;
-import com.redhat.thermostat.common.cli.CommandContext;
-import com.redhat.thermostat.common.cli.CommandException;
-import com.redhat.thermostat.common.cli.TableRenderer;
-import com.redhat.thermostat.notes.client.cli.locale.LocaleResources;
-import com.redhat.thermostat.notes.common.HostNote;
-import com.redhat.thermostat.notes.common.Note;
-import com.redhat.thermostat.notes.common.VmNote;
-import com.redhat.thermostat.shared.locale.LocalizedString;
-import com.redhat.thermostat.storage.core.AgentId;
-import com.redhat.thermostat.storage.core.HostRef;
-import com.redhat.thermostat.storage.core.VmId;
-import com.redhat.thermostat.storage.core.VmRef;
-
-import java.io.PrintStream;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.EnumSet;
-import java.util.List;
-
-public class ListNotesCommand extends AbstractNotesCommand {
-
-    static final String NAME = "list-notes";
-    static final String SHOW_NOTE_ID_ARGUMENT = "show-note-id";
-    static final String QUIET_ARGUMENT = "quiet";
-
-    @Override
-    public void run(CommandContext ctx) throws CommandException {
-        setupServices();
-
-        Arguments args = ctx.getArguments();
-        assertExpectedAgentAndVmArgsProvided(args);
-
-        EnumSet<Field> enabledFields = EnumSet.of(Field.TIMESTAMP, Field.CONTENT);
-
-        if (ctx.getArguments().hasArgument(SHOW_NOTE_ID_ARGUMENT)) {
-            enabledFields.add(Field.NOTE_ID);
-        }
-
-        if (ctx.getArguments().hasArgument(QUIET_ARGUMENT)) {
-            enabledFields.clear();
-            enabledFields.add(Field.NOTE_ID);
-        }
-
-        List<? extends Note> notes;
-        if (args.hasArgument(VmArgument.ARGUMENT_NAME)) {
-            VmId vmId = VmArgument.required(args).getVmId();
-            checkVmExists(vmId);
-            AgentId agentId = new AgentId(vmInfoDAO.getVmInfo(vmId).getAgentId());
-            checkAgentExists(agentId);
-            notes = getVmNotes(getVmRefFromVmId(vmId));
-        } else {
-            AgentId agentId = AgentArgument.required(args).getAgentId();
-            checkAgentExists(agentId);
-            notes = getHostNotes(getHostRefFromAgentId(agentId));
-        }
-
-        Collections.sort(notes, new Comparator<Note>() {
-            @Override
-            public int compare(Note t1, Note t2) {
-                return Long.compare(t1.getTimeStamp(), t2.getTimeStamp());
-            }
-        });
-        printTable(ctx.getConsole().getOutput(), enabledFields, notes);
-    }
-
-    private List<VmNote> getVmNotes(VmRef vmRef) {
-        return vmNoteDAO.getFor(vmRef);
-    }
-
-    private List<HostNote> getHostNotes(HostRef hostRef) {
-        return hostNoteDAO.getFor(hostRef);
-    }
-
-    private void printTable(PrintStream printStream, Collection<Field> enabledFields, List<? extends Note> notes) {
-        if (notes.isEmpty()) {
-            return;
-        }
-        TableRenderer renderer = new TableRenderer(enabledFields.size());
-
-        List<String> header = new ArrayList<>();
-        for (Field field : enabledFields) {
-            header.add(field.getHeader());
-        }
-        renderer.printHeader(header.toArray(new String[header.size()]));
-
-        for (Note note : notes) {
-            List<String> fields = new ArrayList<>();
-            for (Field field : enabledFields) {
-                fields.add(field.extractFrom(note));
-            }
-            renderer.printLine(fields.toArray(new String[fields.size()]));
-        }
-
-        renderer.render(printStream);
-    }
-
-    enum Field {
-        NOTE_ID(translator.localize(LocaleResources.NOTE_ID_COLUMN), new NoteIdMapper()),
-        TIMESTAMP(translator.localize(LocaleResources.TIMESTAMP_COLUMN), new TimestampMapper()),
-        CONTENT(translator.localize(LocaleResources.CONTENT_COLUMN), new ContentMapper()),;
-
-        private final LocalizedString columnName;
-        private final Function<Note, String> cellProvider;
-
-        Field(LocalizedString columnName, Function<Note, String> cellProvider) {
-            this.columnName = columnName;
-            this.cellProvider = cellProvider;
-        }
-
-        public String getHeader() {
-            return columnName.getContents();
-        }
-
-        public String extractFrom(Note note) {
-            return cellProvider.apply(note);
-        }
-    }
-
-    interface Function<I, O> {
-        O apply(I i);
-    }
-
-    static class NoteIdMapper implements Function<Note, String> {
-        @Override
-        public String apply(Note note) {
-            return note.getId();
-        }
-    }
-
-    static class TimestampMapper implements Function<Note, String> {
-        @Override
-        public String apply(Note note) {
-            return Clock.DEFAULT_DATE_FORMAT.format(new Date(note.getTimeStamp()));
-        }
-    }
-
-    static class ContentMapper implements Function<Note, String> {
-        @Override
-        public String apply(Note note) {
-            return note.getContent();
-        }
-    }
-
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/ListNotesSubcommand.java	Fri Oct 14 15:28:23 2016 -0400
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2012-2016 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.notes.client.cli.internal;
+
+import com.redhat.thermostat.client.cli.AgentArgument;
+import com.redhat.thermostat.client.cli.VmArgument;
+import com.redhat.thermostat.common.Clock;
+import com.redhat.thermostat.common.cli.Arguments;
+import com.redhat.thermostat.common.cli.CommandContext;
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.common.cli.TableRenderer;
+import com.redhat.thermostat.notes.client.cli.locale.LocaleResources;
+import com.redhat.thermostat.notes.common.HostNote;
+import com.redhat.thermostat.notes.common.HostNoteDAO;
+import com.redhat.thermostat.notes.common.Note;
+import com.redhat.thermostat.notes.common.VmNote;
+import com.redhat.thermostat.notes.common.VmNoteDAO;
+import com.redhat.thermostat.shared.locale.LocalizedString;
+import com.redhat.thermostat.storage.core.AgentId;
+import com.redhat.thermostat.storage.core.HostRef;
+import com.redhat.thermostat.storage.core.VmId;
+import com.redhat.thermostat.storage.core.VmRef;
+import com.redhat.thermostat.storage.dao.VmInfoDAO;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.List;
+
+public class ListNotesSubcommand extends NotesSubcommand {
+
+    static final String SUBCOMMAND_NAME = "list";
+    static final String SHOW_NOTE_ID_ARGUMENT = "show-note-id";
+    static final String QUIET_ARGUMENT = "quiet";
+
+    @Override
+    public void run(CommandContext ctx) throws CommandException {
+        Arguments args = ctx.getArguments();
+        assertExpectedAgentAndVmArgsProvided(args);
+
+        EnumSet<Field> enabledFields = EnumSet.of(Field.TIMESTAMP, Field.CONTENT);
+
+        if (ctx.getArguments().hasArgument(SHOW_NOTE_ID_ARGUMENT)) {
+            enabledFields.add(Field.NOTE_ID);
+        }
+
+        if (ctx.getArguments().hasArgument(QUIET_ARGUMENT)) {
+            enabledFields.clear();
+            enabledFields.add(Field.NOTE_ID);
+        }
+
+        List<? extends Note> notes;
+        if (args.hasArgument(VmArgument.ARGUMENT_NAME)) {
+            VmInfoDAO vmInfoDao = services.getRequiredService(VmInfoDAO.class);
+
+            VmId vmId = VmArgument.required(args).getVmId();
+            checkVmExists(vmId);
+            AgentId agentId = new AgentId(vmInfoDao.getVmInfo(vmId).getAgentId());
+            checkAgentExists(agentId);
+            notes = getVmNotes(getVmRefFromVmId(vmId));
+        } else {
+            AgentId agentId = AgentArgument.required(args).getAgentId();
+            checkAgentExists(agentId);
+            notes = getHostNotes(getHostRefFromAgentId(agentId));
+        }
+
+        Collections.sort(notes, new Comparator<Note>() {
+            @Override
+            public int compare(Note t1, Note t2) {
+                return Long.compare(t1.getTimeStamp(), t2.getTimeStamp());
+            }
+        });
+        printTable(ctx.getConsole().getOutput(), enabledFields, notes);
+    }
+
+    private List<VmNote> getVmNotes(VmRef vmRef) throws CommandException {
+        return services.getRequiredService(VmNoteDAO.class).getFor(vmRef);
+    }
+
+    private List<HostNote> getHostNotes(HostRef hostRef) throws CommandException {
+        return services.getRequiredService(HostNoteDAO.class).getFor(hostRef);
+    }
+
+    private void printTable(PrintStream printStream, Collection<Field> enabledFields, List<? extends Note> notes) {
+        if (notes.isEmpty()) {
+            return;
+        }
+        TableRenderer renderer = new TableRenderer(enabledFields.size());
+
+        List<String> header = new ArrayList<>();
+        for (Field field : enabledFields) {
+            header.add(field.getHeader());
+        }
+        renderer.printHeader(header.toArray(new String[header.size()]));
+
+        for (Note note : notes) {
+            List<String> fields = new ArrayList<>();
+            for (Field field : enabledFields) {
+                fields.add(field.extractFrom(note));
+            }
+            renderer.printLine(fields.toArray(new String[fields.size()]));
+        }
+
+        renderer.render(printStream);
+    }
+
+    private enum Field {
+        NOTE_ID(translator.localize(LocaleResources.NOTE_ID_COLUMN), new NoteIdMapper()),
+        TIMESTAMP(translator.localize(LocaleResources.TIMESTAMP_COLUMN), new TimestampMapper()),
+        CONTENT(translator.localize(LocaleResources.CONTENT_COLUMN), new ContentMapper()),;
+
+        private final LocalizedString columnName;
+        private final Function<Note, String> cellProvider;
+
+        Field(LocalizedString columnName, Function<Note, String> cellProvider) {
+            this.columnName = columnName;
+            this.cellProvider = cellProvider;
+        }
+
+        public String getHeader() {
+            return columnName.getContents();
+        }
+
+        public String extractFrom(Note note) {
+            return cellProvider.apply(note);
+        }
+    }
+
+    interface Function<I, O> {
+        O apply(I i);
+    }
+
+    private static class NoteIdMapper implements Function<Note, String> {
+        @Override
+        public String apply(Note note) {
+            return note.getId();
+        }
+    }
+
+    private static class TimestampMapper implements Function<Note, String> {
+        @Override
+        public String apply(Note note) {
+            return Clock.DEFAULT_DATE_FORMAT.format(new Date(note.getTimeStamp()));
+        }
+    }
+
+    private static class ContentMapper implements Function<Note, String> {
+        @Override
+        public String apply(Note note) {
+            return note.getContent();
+        }
+    }
+
+}
--- a/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/NoteIdCompleterService.java	Thu Nov 17 12:47:48 2016 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,78 +0,0 @@
-/*
- * Copyright 2012-2016 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.notes.client.cli.internal;
-
-import com.redhat.thermostat.common.cli.AbstractCompleterService;
-import com.redhat.thermostat.common.cli.CliCommandOption;
-import com.redhat.thermostat.common.cli.CompletionFinderTabCompleter;
-import com.redhat.thermostat.common.cli.TabCompleter;
-import com.redhat.thermostat.notes.common.HostNoteDAO;
-import com.redhat.thermostat.notes.common.VmNoteDAO;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-public class NoteIdCompleterService extends AbstractCompleterService {
-
-    public static final CliCommandOption NOTE_ID_OPTION = new CliCommandOption("n", "noteId", true, "Note ID", false);
-
-    @Override
-    public Set<String> getCommands() {
-        return new HashSet<>(Arrays.asList(
-            DeleteNoteCommand.NAME,
-            UpdateNoteCommand.NAME
-        ));
-    }
-
-    @Override
-    public Map<CliCommandOption, ? extends TabCompleter> getOptionCompleters() {
-        TabCompleter completer = new CompletionFinderTabCompleter(new NoteIdsFinder(dependencyServices));
-        return Collections.singletonMap(NOTE_ID_OPTION, completer);
-    }
-
-    public void setHostNoteDao(HostNoteDAO hostNoteDao) {
-        setService(HostNoteDAO.class, hostNoteDao);
-    }
-
-    public void setVmNoteDao(VmNoteDAO vmNoteDao) {
-        setService(VmNoteDAO.class, vmNoteDao);
-    }
-
-}
--- a/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/NoteIdsFinder.java	Thu Nov 17 12:47:48 2016 -0500
+++ b/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/NoteIdsFinder.java	Fri Oct 14 15:28:23 2016 -0400
@@ -36,9 +36,8 @@
 
 package com.redhat.thermostat.notes.client.cli.internal;
 
-import com.redhat.thermostat.common.cli.AbstractCompletionFinder;
+import com.redhat.thermostat.common.cli.CompletionFinder;
 import com.redhat.thermostat.common.cli.CompletionInfo;
-import com.redhat.thermostat.common.cli.DependencyServices;
 import com.redhat.thermostat.notes.client.cli.locale.LocaleResources;
 import com.redhat.thermostat.notes.common.HostNoteDAO;
 import com.redhat.thermostat.notes.common.Note;
@@ -49,31 +48,22 @@
 import java.util.Collections;
 import java.util.List;
 
-public class NoteIdsFinder extends AbstractCompletionFinder {
+public class NoteIdsFinder implements CompletionFinder {
 
     private static final Translate<LocaleResources> t = LocaleResources.createLocalizer();
 
-    public NoteIdsFinder(DependencyServices dependencyServices) {
-        super(dependencyServices);
-    }
-
-    @Override
-    protected Class<?>[] getRequiredDependencies() {
-        return new Class<?>[]{ HostNoteDAO.class, VmNoteDAO.class };
-    }
+    private HostNoteDAO hostNoteDao;
+    private VmNoteDAO vmNoteDao;
 
     @Override
     public List<CompletionInfo> findCompletions() {
-        if (!allDependenciesAvailable()) {
+        if (hostNoteDao == null || vmNoteDao == null) {
             return Collections.emptyList();
         }
 
-        HostNoteDAO host = getService(HostNoteDAO.class);
-        VmNoteDAO vm = getService(VmNoteDAO.class);
-
         List<Note> notes = new ArrayList<>();
-        notes.addAll(host.getAll());
-        notes.addAll(vm.getAll());
+        notes.addAll(hostNoteDao.getAll());
+        notes.addAll(vmNoteDao.getAll());
 
         List<CompletionInfo> result = new ArrayList<>();
         for (Note note : notes) {
@@ -93,4 +83,25 @@
         return t.localize(LocaleResources.TRIMMED_NOTE_CONTENT, content.substring(0, 27)).getContents();
     }
 
+    public void bindHostNoteDao(HostNoteDAO hostNoteDao) {
+        this.hostNoteDao = hostNoteDao;
+    }
+
+    public void unbindHostNoteDao(HostNoteDAO hostNoteDao) {
+        this.hostNoteDao = null;
+    }
+
+    public void bindVmNoteDao(VmNoteDAO vmNoteDao) {
+        this.vmNoteDao = vmNoteDao;
+    }
+
+    public void unbindVmNoteDao(VmNoteDAO vmNoteDao) {
+        this.vmNoteDao = null;
+    }
+
+    public void servicesUnavailable() {
+        this.hostNoteDao = null;
+        this.vmNoteDao = null;
+    }
+
 }
--- a/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/NotesCommand.java	Thu Nov 17 12:47:48 2016 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,63 +0,0 @@
-/*
- * Copyright 2012-2016 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.notes.client.cli.internal;
-
-import com.redhat.thermostat.common.cli.Command;
-import com.redhat.thermostat.notes.common.HostNoteDAO;
-import com.redhat.thermostat.notes.common.VmNoteDAO;
-import com.redhat.thermostat.storage.dao.AgentInfoDAO;
-import com.redhat.thermostat.storage.dao.HostInfoDAO;
-import com.redhat.thermostat.storage.dao.VmInfoDAO;
-
-public interface NotesCommand extends Command {
-
-    String NOTE_ID_ARGUMENT = "noteId";
-    String NOTE_CONTENT_ARGUMENT = "content";
-
-    void setVmInfoDao(VmInfoDAO vmInfoDao);
-
-    void setHostInfoDao(HostInfoDAO hostInfoDao);
-
-    void setAgentInfoDao(AgentInfoDAO agentInfoDao);
-
-    void setVmNoteDao(VmNoteDAO vmNoteDao);
-
-    void setHostNoteDao(HostNoteDAO hostNoteDao);
-
-    void servicesUnavailable();
-
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/NotesControlCommand.java	Fri Oct 14 15:28:23 2016 -0400
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2012-2016 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.notes.client.cli.internal;
+
+import com.redhat.thermostat.common.cli.AbstractCommand;
+import com.redhat.thermostat.common.cli.Arguments;
+import com.redhat.thermostat.common.cli.CliCommandOption;
+import com.redhat.thermostat.common.cli.CommandContext;
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.common.cli.CompleterService;
+import com.redhat.thermostat.common.cli.CompletionFinderTabCompleter;
+import com.redhat.thermostat.common.cli.TabCompleter;
+import com.redhat.thermostat.notes.client.cli.locale.LocaleResources;
+import com.redhat.thermostat.shared.locale.Translate;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class NotesControlCommand extends AbstractCommand implements CompleterService {
+
+    public static final String COMMAND_NAME = "notes";
+    static final CliCommandOption NOTE_ID_OPTION = new CliCommandOption("n", "noteId", true, "Note ID", false);
+
+    private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
+
+    private NoteIdsFinder noteIdsFinder;
+
+    private AddNoteSubcommand addNoteCommand;
+    private DeleteNoteSubcommand deleteNoteCommand;
+    private UpdateNoteSubcommand updateNoteCommand;
+    private ListNotesSubcommand listNotesCommand;
+
+    public NotesControlCommand(NoteIdsFinder noteIdsFinder, AddNoteSubcommand addNoteCommand,
+                               DeleteNoteSubcommand deleteNoteCommand, UpdateNoteSubcommand updateNoteCommand,
+                               ListNotesSubcommand listNotesCommand) {
+        this.noteIdsFinder = noteIdsFinder;
+        this.addNoteCommand = addNoteCommand;
+        this.deleteNoteCommand = deleteNoteCommand;
+        this.updateNoteCommand = updateNoteCommand;
+        this.listNotesCommand = listNotesCommand;
+    }
+
+    @Override
+    public Set<String> getCommands() {
+        return Collections.singleton(COMMAND_NAME);
+    }
+
+    @Override
+    public Map<CliCommandOption, ? extends TabCompleter> getOptionCompleters() {
+        return Collections.emptyMap();
+    }
+
+    @Override
+    public Map<String, Map<CliCommandOption, ? extends TabCompleter>> getSubcommandCompleters() {
+        Map<CliCommandOption, ? extends TabCompleter> noteIdCompletion =
+                Collections.singletonMap(NOTE_ID_OPTION, new CompletionFinderTabCompleter(noteIdsFinder));
+
+        Map<String, Map<CliCommandOption, ? extends TabCompleter>> completions = new HashMap<>();
+        completions.put(UpdateNoteSubcommand.SUBCOMMAND_NAME, noteIdCompletion);
+        completions.put(DeleteNoteSubcommand.SUBCOMMAND_NAME, noteIdCompletion);
+        return completions;
+    }
+
+    @Override
+    public void run(CommandContext ctx) throws CommandException {
+        Arguments args = ctx.getArguments();
+        List<String> nonOptionArgs = args.getNonOptionArguments();
+        if (nonOptionArgs.isEmpty()) {
+            throw new CommandException(translator.localize(LocaleResources.SUBCOMMAND_EXPECTED));
+        }
+
+        String subcommand = nonOptionArgs.get(0);
+        switch (subcommand) {
+            case AddNoteSubcommand.SUBCOMMAND_NAME:
+                addNoteCommand.run(ctx);
+                break;
+            case DeleteNoteSubcommand.SUBCOMMAND_NAME:
+                deleteNoteCommand.run(ctx);
+                break;
+            case UpdateNoteSubcommand.SUBCOMMAND_NAME:
+                updateNoteCommand.run(ctx);
+                break;
+            case ListNotesSubcommand.SUBCOMMAND_NAME:
+                listNotesCommand.run(ctx);
+                break;
+            default:
+                throw new CommandException(translator.localize(LocaleResources.UNKNOWN_SUBCOMMAND, subcommand));
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/NotesSubcommand.java	Fri Oct 14 15:28:23 2016 -0400
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2012-2016 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.notes.client.cli.internal;
+
+import com.redhat.thermostat.client.cli.AgentArgument;
+import com.redhat.thermostat.client.cli.VmArgument;
+import com.redhat.thermostat.common.cli.Arguments;
+import com.redhat.thermostat.common.cli.CommandContext;
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.common.cli.DependencyServices;
+import com.redhat.thermostat.notes.client.cli.locale.LocaleResources;
+import com.redhat.thermostat.notes.common.HostNoteDAO;
+import com.redhat.thermostat.notes.common.VmNoteDAO;
+import com.redhat.thermostat.shared.locale.LocalizedString;
+import com.redhat.thermostat.shared.locale.Translate;
+import com.redhat.thermostat.storage.core.AgentId;
+import com.redhat.thermostat.storage.core.HostRef;
+import com.redhat.thermostat.storage.core.VmId;
+import com.redhat.thermostat.storage.core.VmRef;
+import com.redhat.thermostat.storage.dao.AgentInfoDAO;
+import com.redhat.thermostat.storage.dao.HostInfoDAO;
+import com.redhat.thermostat.storage.dao.VmInfoDAO;
+import com.redhat.thermostat.storage.model.AgentInformation;
+import com.redhat.thermostat.storage.model.VmInfo;
+
+public abstract class NotesSubcommand {
+
+    static final String NOTE_ID_ARGUMENT = "noteId";
+    static final String NOTE_CONTENT_ARGUMENT = "content";
+
+    static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
+
+    DependencyServices services = new DependencyServices();
+
+    public abstract void run(CommandContext ctx) throws CommandException;
+
+    static void assertExpectedAgentAndVmArgsProvided(Arguments args) throws CommandException {
+        boolean hasVmArg = args.hasArgument(VmArgument.ARGUMENT_NAME);
+        boolean hasAgentArg = args.hasArgument(AgentArgument.ARGUMENT_NAME);
+        if (hasVmArg && hasAgentArg) {
+            throw new CommandException(translator.localize(LocaleResources.HOST_AND_VM_ARGS_PROVIDED));
+        } else if (!(hasVmArg || hasAgentArg)) {
+            throw new CommandException(translator.localize(LocaleResources.NO_ARGS_PROVIDED));
+        }
+    }
+
+    void checkVmExists(VmId vmId) throws CommandException {
+        requireNonNull(vmId, translator.localize(LocaleResources.NULL_VMID));
+        VmInfo info = services.getRequiredService(VmInfoDAO.class).getVmInfo(vmId);
+        requireNonNull(info, translator.localize(LocaleResources.INVALID_VMID, vmId.get()));
+    }
+
+    void checkAgentExists(AgentId agentId) throws CommandException {
+        requireNonNull(agentId, translator.localize(LocaleResources.NULL_AGENTID));
+        AgentInformation info = services.getRequiredService(AgentInfoDAO.class).getAgentInformation(agentId);
+        requireNonNull(info, translator.localize(LocaleResources.INVALID_AGENTID, agentId.get()));
+    }
+
+    VmRef getVmRefFromVmId(VmId vmId) throws CommandException {
+        requireNonNull(vmId, translator.localize(LocaleResources.NULL_VMID));
+        VmInfo vmInfo = services.getRequiredService(VmInfoDAO.class).getVmInfo(vmId);
+        return new VmRef(getHostRefFromAgentId(new AgentId(vmInfo.getAgentId())), vmId.get(), vmInfo.getVmPid(), vmInfo.getVmName());
+    }
+
+    HostRef getHostRefFromAgentId(AgentId agentId) throws CommandException {
+        String hostName = services.getRequiredService(HostInfoDAO.class).getHostInfo(agentId).getHostname();
+        return new HostRef(agentId.get(), hostName);
+    }
+
+    static String getNoteId(Arguments args) throws CommandException {
+        if (!args.hasArgument(NOTE_ID_ARGUMENT)) {
+            throw new CommandException(translator.localize(LocaleResources.NOTE_ID_ARG_REQUIRED));
+        }
+        return args.getArgument(NOTE_ID_ARGUMENT);
+    }
+
+    static String getNoteContent(Arguments args) throws CommandException {
+        if (!args.hasArgument(NOTE_CONTENT_ARGUMENT)) {
+            throw new CommandException(translator.localize(LocaleResources.NOTE_CONTENT_ARG_REQUIRED));
+        }
+        return args.getArgument(NOTE_CONTENT_ARGUMENT);
+    }
+
+    static void requireNonNull(Object obj, LocalizedString message) throws CommandException {
+        if (obj == null) {
+            throw new CommandException(message);
+        }
+    }
+
+    public void bindVmInfoDao(VmInfoDAO vmInfoDao) {
+        services.addService(VmInfoDAO.class, vmInfoDao);
+    }
+
+    public void unbindVmInfoDao(VmInfoDAO vmInfoDao) {
+        services.removeService(VmInfoDAO.class);
+    }
+
+    public void bindHostInfoDao(HostInfoDAO hostInfoDao) {
+        services.addService(HostInfoDAO.class, hostInfoDao);
+    }
+
+    public void unbindHostInfoDao(HostInfoDAO hostInfoDao) {
+        services.removeService(HostInfoDAO.class);
+    }
+
+    public void bindAgentInfoDao(AgentInfoDAO agentInfoDao) {
+        services.addService(AgentInfoDAO.class, agentInfoDao);
+    }
+
+    public void unbindAgentInfoDao(AgentInfoDAO agentInfoDao) {
+        services.removeService(AgentInfoDAO.class);
+    }
+
+    public void bindVmNoteDao(VmNoteDAO vmNoteDAO) {
+        services.addService(VmNoteDAO.class, vmNoteDAO);
+    }
+
+    public void unbindVmNoteDao(VmNoteDAO vmNoteDao) {
+        services.removeService(VmNoteDAO.class);
+    }
+
+    public void bindHostNoteDao(HostNoteDAO hostNoteDAO) {
+        services.addService(HostNoteDAO.class, hostNoteDAO);
+    }
+
+    public void unbindHostNoteDao(HostNoteDAO hostNoteDao) {
+        services.removeService(HostNoteDAO.class);
+    }
+
+    public void servicesUnavailable() {
+        services.removeService(VmInfoDAO.class);
+        services.removeService(HostInfoDAO.class);
+        services.removeService(AgentInfoDAO.class);
+        services.removeService(VmNoteDAO.class);
+        services.removeService(HostNoteDAO.class);
+    }
+
+}
--- a/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/UpdateNoteCommand.java	Thu Nov 17 12:47:48 2016 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,91 +0,0 @@
-/*
- * Copyright 2012-2016 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.notes.client.cli.internal;
-
-import com.redhat.thermostat.client.cli.AgentArgument;
-import com.redhat.thermostat.client.cli.VmArgument;
-import com.redhat.thermostat.common.cli.Arguments;
-import com.redhat.thermostat.common.cli.CommandContext;
-import com.redhat.thermostat.common.cli.CommandException;
-import com.redhat.thermostat.notes.client.cli.locale.LocaleResources;
-import com.redhat.thermostat.notes.common.HostNote;
-import com.redhat.thermostat.notes.common.VmNote;
-import com.redhat.thermostat.storage.core.AgentId;
-import com.redhat.thermostat.storage.core.VmId;
-
-public class UpdateNoteCommand extends AbstractNotesCommand {
-
-    static final String NAME = "update-note";
-
-    @Override
-    public void run(CommandContext ctx) throws CommandException {
-        setupServices();
-
-        Arguments args = ctx.getArguments();
-        assertExpectedAgentAndVmArgsProvided(args);
-
-        String noteId = getNoteId(args);
-        String noteContent = getNoteContent(args);
-
-        if (args.hasArgument(VmArgument.ARGUMENT_NAME)) {
-            VmId vmId = VmArgument.required(args).getVmId();
-            checkVmExists(vmId);
-            AgentId agentId = new AgentId(vmInfoDAO.getVmInfo(vmId).getAgentId());
-            checkAgentExists(agentId);
-
-            VmNote note = vmNoteDAO.getById(getVmRefFromVmId(vmId), noteId);
-            if (note == null) {
-                throw new CommandException(translator.localize(LocaleResources.NO_SUCH_VM_NOTE, vmId.get(), noteId));
-            }
-            note.setContent(noteContent);
-            note.setTimeStamp(System.currentTimeMillis());
-            vmNoteDAO.update(note);
-        } else {
-            AgentId agentId = AgentArgument.required(args).getAgentId();
-            checkAgentExists(agentId);
-
-            HostNote note = hostNoteDAO.getById(getHostRefFromAgentId(agentId), noteId);
-            if (note == null) {
-                throw new CommandException(translator.localize(LocaleResources.NO_SUCH_AGENT_NOTE, agentId.get(), noteId));
-            }
-            note.setContent(noteContent);
-            note.setTimeStamp(System.currentTimeMillis());
-            hostNoteDAO.update(note);
-        }
-    }
-
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/internal/UpdateNoteSubcommand.java	Fri Oct 14 15:28:23 2016 -0400
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2012-2016 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.notes.client.cli.internal;
+
+import com.redhat.thermostat.client.cli.AgentArgument;
+import com.redhat.thermostat.client.cli.VmArgument;
+import com.redhat.thermostat.common.cli.Arguments;
+import com.redhat.thermostat.common.cli.CommandContext;
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.notes.client.cli.locale.LocaleResources;
+import com.redhat.thermostat.notes.common.HostNote;
+import com.redhat.thermostat.notes.common.HostNoteDAO;
+import com.redhat.thermostat.notes.common.VmNote;
+import com.redhat.thermostat.notes.common.VmNoteDAO;
+import com.redhat.thermostat.storage.core.AgentId;
+import com.redhat.thermostat.storage.core.VmId;
+import com.redhat.thermostat.storage.dao.VmInfoDAO;
+
+public class UpdateNoteSubcommand extends NotesSubcommand {
+
+    static final String SUBCOMMAND_NAME = "update";
+
+    @Override
+    public void run(CommandContext ctx) throws CommandException {
+        Arguments args = ctx.getArguments();
+        assertExpectedAgentAndVmArgsProvided(args);
+
+        String noteId = getNoteId(args);
+        String noteContent = getNoteContent(args);
+
+        if (args.hasArgument(VmArgument.ARGUMENT_NAME)) {
+            VmNoteDAO vmNoteDao = services.getRequiredService(VmNoteDAO.class);
+            VmInfoDAO vmInfoDao = services.getRequiredService(VmInfoDAO.class);
+
+            VmId vmId = VmArgument.required(args).getVmId();
+            checkVmExists(vmId);
+            AgentId agentId = new AgentId(vmInfoDao.getVmInfo(vmId).getAgentId());
+            checkAgentExists(agentId);
+
+            VmNote note = vmNoteDao.getById(getVmRefFromVmId(vmId), noteId);
+            if (note == null) {
+                throw new CommandException(translator.localize(LocaleResources.NO_SUCH_VM_NOTE, vmId.get(), noteId));
+            }
+            note.setContent(noteContent);
+            note.setTimeStamp(System.currentTimeMillis());
+            vmNoteDao.update(note);
+        } else {
+            HostNoteDAO hostNoteDao = services.getRequiredService(HostNoteDAO.class);
+
+            AgentId agentId = AgentArgument.required(args).getAgentId();
+            checkAgentExists(agentId);
+
+            HostNote note = hostNoteDao.getById(getHostRefFromAgentId(agentId), noteId);
+            if (note == null) {
+                throw new CommandException(translator.localize(LocaleResources.NO_SUCH_AGENT_NOTE, agentId.get(), noteId));
+            }
+            note.setContent(noteContent);
+            note.setTimeStamp(System.currentTimeMillis());
+            hostNoteDao.update(note);
+        }
+    }
+
+}
--- a/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/locale/LocaleResources.java	Thu Nov 17 12:47:48 2016 -0500
+++ b/notes/client-cli/src/main/java/com/redhat/thermostat/notes/client/cli/locale/LocaleResources.java	Fri Oct 14 15:28:23 2016 -0400
@@ -40,10 +40,15 @@
 
 public enum LocaleResources {
 
+    SUBCOMMAND_EXPECTED,
+    UNKNOWN_SUBCOMMAND,
+
     HOST_AND_VM_ARGS_PROVIDED,
     NO_ARGS_PROVIDED,
     NOTE_ID_ARG_REQUIRED,
 
+    NULL_VMID,
+    NULL_AGENTID,
     INVALID_VMID,
     INVALID_AGENTID,
 
--- a/notes/client-cli/src/main/resources/com/redhat/thermostat/notes/client/cli/locale/strings.properties	Thu Nov 17 12:47:48 2016 -0500
+++ b/notes/client-cli/src/main/resources/com/redhat/thermostat/notes/client/cli/locale/strings.properties	Fri Oct 14 15:28:23 2016 -0400
@@ -1,7 +1,12 @@
+SUBCOMMAND_EXPECTED=A subcommand name is expected
+UNKNOWN_SUBCOMMAND=The subcommand "{0}" is not recognized
+
 HOST_AND_VM_ARGS_PROVIDED=Both a Host ID and VM ID were provided, but only expected one
 NO_ARGS_PROVIDED=A Host or VM ID must be provided
 NOTE_ID_ARG_REQUIRED=Note ID argument must be provided
 
+NULL_VMID=The provided vmId must not be null
+NULL_AGENTID=The provided agentId must not be null
 INVALID_VMID=No VM exists with the specified ID: {0}
 INVALID_AGENTID=No Agent exists with the specified ID: {0}
 
--- a/notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/AbstractNotesCommandTest.java	Thu Nov 17 12:47:48 2016 -0500
+++ b/notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/AbstractNotesCommandTest.java	Fri Oct 14 15:28:23 2016 -0400
@@ -56,7 +56,6 @@
 import com.redhat.thermostat.storage.model.AgentInformation;
 import com.redhat.thermostat.storage.model.HostInfo;
 import com.redhat.thermostat.storage.model.VmInfo;
-
 import org.junit.Before;
 import org.junit.Test;
 
@@ -73,20 +72,20 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
-public abstract class AbstractNotesCommandTest<T extends AbstractNotesCommand> {
+public abstract class AbstractNotesCommandTest<T extends NotesSubcommand> {
 
-    protected static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
+    private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
 
-    protected static final String INVALID_AGENTID_MSG = translator.localize(LocaleResources.INVALID_AGENTID).getContents().replaceAll("\\{0\\}", "");
-    protected static final String INVALID_VMID_MSG = translator.localize(LocaleResources.INVALID_VMID).getContents().replaceAll("\\{0\\}", "");
+    private static final String INVALID_AGENTID_MSG = translator.localize(LocaleResources.INVALID_AGENTID).getContents().replaceAll("\\{0\\}", "");
+    private static final String INVALID_VMID_MSG = translator.localize(LocaleResources.INVALID_VMID).getContents().replaceAll("\\{0\\}", "");
 
-    protected VmInfoDAO vmInfoDAO;
-    protected HostInfoDAO hostInfoDAO;
-    protected AgentInfoDAO agentInfoDAO;
-    protected VmNoteDAO vmNoteDAO;
-    protected HostNoteDAO hostNoteDAO;
+    VmInfoDAO vmInfoDAO;
+    HostInfoDAO hostInfoDAO;
+    AgentInfoDAO agentInfoDAO;
+    VmNoteDAO vmNoteDAO;
+    HostNoteDAO hostNoteDAO;
 
-    protected TestCommandContextFactory contextFactory;
+    TestCommandContextFactory contextFactory;
 
     protected T command;
 
@@ -101,75 +100,11 @@
         contextFactory = new TestCommandContextFactory();
 
         command = createCommand();
-        setAllServices();
-    }
-
-    protected void setAllServices() {
-        command.setVmInfoDao(vmInfoDAO);
-        command.setHostInfoDao(hostInfoDAO);
-        command.setAgentInfoDao(agentInfoDAO);
-        command.setVmNoteDao(vmNoteDAO);
-        command.setHostNoteDao(hostNoteDAO);
-        try {
-            command.setupServices();
-        } catch (CommandException e) {
-            fail("Service setup should not fail here!");
-        }
-    }
-
-    @Test
-    public void testSetupServicesSucceedsWhenAllPresent() throws CommandException {
-        command.setupServices();
-    }
-
-    @Test(expected = CommandException.class)
-    public void testSetupServicesFailsWhenVmInfoMissing() throws CommandException {
-        command.servicesUnavailable();
-        command.setHostInfoDao(hostInfoDAO);
-        command.setAgentInfoDao(agentInfoDAO);
-        command.setVmNoteDao(vmNoteDAO);
-        command.setHostNoteDao(hostNoteDAO);
-        command.setupServices();
-    }
-
-    @Test(expected = CommandException.class)
-    public void testSetupServicesFailsWhenHostInfoMissing() throws CommandException {
-        command.servicesUnavailable();
-        command.setVmInfoDao(vmInfoDAO);
-        command.setAgentInfoDao(agentInfoDAO);
-        command.setVmNoteDao(vmNoteDAO);
-        command.setHostNoteDao(hostNoteDAO);
-        command.setupServices();
-    }
-
-    @Test(expected = CommandException.class)
-    public void testSetupServicesFailsWhenAgentInfoMissing() throws CommandException {
-        command.servicesUnavailable();
-        command.setVmInfoDao(vmInfoDAO);
-        command.setHostInfoDao(hostInfoDAO);
-        command.setVmNoteDao(vmNoteDAO);
-        command.setHostNoteDao(hostNoteDAO);
-        command.setupServices();
-    }
-
-    @Test(expected = CommandException.class)
-    public void testSetupServicesFailsWhenVmNoteMissing() throws CommandException {
-        command.servicesUnavailable();
-        command.setVmInfoDao(vmInfoDAO);
-        command.setHostInfoDao(hostInfoDAO);
-        command.setAgentInfoDao(agentInfoDAO);
-        command.setHostNoteDao(hostNoteDAO);
-        command.setupServices();
-    }
-
-    @Test(expected = CommandException.class)
-    public void testSetupServicesFailsWhenHostNoteMissing() throws CommandException {
-        command.servicesUnavailable();
-        command.setVmInfoDao(vmInfoDAO);
-        command.setHostInfoDao(hostInfoDAO);
-        command.setAgentInfoDao(agentInfoDAO);
-        command.setVmNoteDao(vmNoteDAO);
-        command.setupServices();
+        command.bindVmInfoDao(vmInfoDAO);
+        command.bindHostInfoDao(hostInfoDAO);
+        command.bindAgentInfoDao(agentInfoDAO);
+        command.bindVmNoteDao(vmNoteDAO);
+        command.bindHostNoteDao(hostNoteDAO);
     }
 
     @Test
@@ -177,7 +112,7 @@
         Arguments args = mock(Arguments.class);
         when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
         when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(false);
-        AbstractNotesCommand.assertExpectedAgentAndVmArgsProvided(args);
+        NotesSubcommand.assertExpectedAgentAndVmArgsProvided(args);
     }
 
     @Test
@@ -185,7 +120,7 @@
         Arguments args = mock(Arguments.class);
         when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(false);
         when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
-        AbstractNotesCommand.assertExpectedAgentAndVmArgsProvided(args);
+        NotesSubcommand.assertExpectedAgentAndVmArgsProvided(args);
     }
 
     @Test(expected = CommandException.class)
@@ -193,7 +128,7 @@
         Arguments args = mock(Arguments.class);
         when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(false);
         when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(false);
-        AbstractNotesCommand.assertExpectedAgentAndVmArgsProvided(args);
+        NotesSubcommand.assertExpectedAgentAndVmArgsProvided(args);
     }
 
     @Test(expected = CommandException.class)
@@ -201,7 +136,7 @@
         Arguments args = mock(Arguments.class);
         when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
         when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
-        AbstractNotesCommand.assertExpectedAgentAndVmArgsProvided(args);
+        NotesSubcommand.assertExpectedAgentAndVmArgsProvided(args);
     }
 
     @Test
@@ -219,7 +154,7 @@
         }
     }
 
-    @Test(expected = NullPointerException.class)
+    @Test(expected = CommandException.class)
     public void testAssertVmExistsWithNullVmId() throws CommandException {
         command.checkVmExists(null);
     }
@@ -246,7 +181,7 @@
         }
     }
 
-    @Test(expected = NullPointerException.class)
+    @Test(expected = CommandException.class)
     public void testAssertAgentExistsWithNullAgentId() throws CommandException {
         command.checkAgentExists(null);
     }
@@ -258,7 +193,7 @@
     }
 
     @Test
-    public void testGetVmRefFromVmId() {
+    public void testGetVmRefFromVmId() throws CommandException {
         VmInfo vmInfo = new VmInfo();
         vmInfo.setAgentId("foo-agentid");
         vmInfo.setVmId("foo-vmid");
@@ -277,7 +212,7 @@
     }
 
     @Test
-    public void testGetHostRefFromAgentId() {
+    public void testGetHostRefFromAgentId() throws CommandException {
         HostInfo hostInfo = mock(HostInfo.class);
         when(hostInfo.getHostname()).thenReturn("foo-hostname");
         when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
@@ -292,84 +227,43 @@
     @Test(expected = CommandException.class)
     public void testGetNoteIdFailsWhenArgNotProvided() throws CommandException {
         Arguments args = mock(Arguments.class);
-        AbstractNotesCommand.getNoteId(args);
+        NotesSubcommand.getNoteId(args);
     }
 
     @Test
     public void testGetNoteId() throws CommandException {
         Arguments args = mock(Arguments.class);
-        when(args.hasArgument(NotesCommand.NOTE_ID_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(NotesCommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
-        String result = AbstractNotesCommand.getNoteId(args);
+        when(args.hasArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
+        String result = NotesSubcommand.getNoteId(args);
         assertThat(result, is("foo-noteid"));
     }
 
     @Test
-    public void testGetNoteContentWithNoFlagAndNoNonOptionArgs() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(NotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn(false);
-        when(args.getNonOptionArguments()).thenReturn(Collections.<String>emptyList());
-        try {
-            AbstractNotesCommand.getNoteContent(args);
-            fail();
-        } catch (CommandException ex) {
-            // pass
-        }
-    }
-
-    @Test
-    public void testGetNoteContentWithNonOptionArgs() throws CommandException {
+    public void testGetNoteContent() throws CommandException {
         Arguments args = mock(Arguments.class);
-        when(args.hasArgument(NotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn(false);
-        when(args.getNonOptionArguments()).thenReturn(Arrays.asList("this", "is", "a", "note"));
-        String result = AbstractNotesCommand.getNoteContent(args);
-        assertThat(result, is("this is a note"));
-    }
-
-    @Test
-    public void testGetNoteContentWithFlag() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(NotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(NotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn("this is a note");
+        when(args.hasArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn("this is a note");
         when(args.getNonOptionArguments()).thenReturn(Collections.<String>emptyList());
-        String result = AbstractNotesCommand.getNoteContent(args);
-        assertThat(result, is("this is a note"));
-    }
-
-    @Test
-    public void testGetNoteContentWithFlagAndNonOptionArgs() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(NotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(NotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn("this is a note");
-        when(args.getNonOptionArguments()).thenReturn(Arrays.asList("here", "is", "another"));
-        String result = AbstractNotesCommand.getNoteContent(args);
+        String result = NotesSubcommand.getNoteContent(args);
         assertThat(result, is("this is a note"));
     }
 
     @Test
     public void testGetNoteContentWithStrangeInput() throws CommandException {
         Arguments args = mock(Arguments.class);
-        when(args.hasArgument(NotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(NotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn("this is a note");
+        when(args.hasArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn("this is a note");
         when(args.getNonOptionArguments()).thenReturn(Arrays.asList("here", "is", "another", "--noteContent", "strange", "--noteContent", "--input"));
-        String result = AbstractNotesCommand.getNoteContent(args);
+        String result = NotesSubcommand.getNoteContent(args);
         assertThat(result, is("this is a note"));
     }
 
-    @Test
-    public void testGetNoteContentDoesNotReturnNull() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(NotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(NotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn(null);
-        String result = AbstractNotesCommand.getNoteContent(args);
-        assertThat(result, is(""));
-    }
-
-    public void doInvalidAgentIdTest(Arguments args) {
+    void doInvalidAgentIdTest(Arguments args) {
         doInvalidIdTest(args, INVALID_AGENTID_MSG);
     }
 
-    public void doInvalidVmIdTest(Arguments args) {
+    void doInvalidVmIdTest(Arguments args) {
         doInvalidIdTest(args, INVALID_VMID_MSG);
     }
 
--- a/notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/ActivatorTest.java	Thu Nov 17 12:47:48 2016 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-/*
- * Copyright 2012-2016 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.notes.client.cli.internal;
-
-import com.redhat.thermostat.common.cli.CompleterService;
-import com.redhat.thermostat.testutils.StubBundleContext;
-import org.junit.Test;
-
-import static com.redhat.thermostat.testutils.Asserts.assertCommandIsNotRegistered;
-import static com.redhat.thermostat.testutils.Asserts.assertCommandIsRegistered;
-import static com.redhat.thermostat.testutils.Asserts.assertServiceIsRegistered;
-import static org.junit.Assert.assertEquals;
-
-public class ActivatorTest {
-
-    @Test
-    public void testCommandsAreRegistered() throws Exception {
-        StubBundleContext context = new StubBundleContext();
-        Activator activator = new Activator();
-
-        activator.start(context);
-
-        assertCommandIsRegistered(context, AddNoteCommand.NAME, AddNoteCommand.class);
-        assertCommandIsRegistered(context, DeleteNoteCommand.NAME, DeleteNoteCommand.class);
-        assertCommandIsRegistered(context, UpdateNoteCommand.NAME, UpdateNoteCommand.class);
-        assertCommandIsRegistered(context, ListNotesCommand.NAME, ListNotesCommand.class);
-        assertServiceIsRegistered(context, CompleterService.class, NoteIdCompleterService.class);
-
-        assertEquals(5, context.getAllServices().size());
-
-        activator.stop(context);
-
-        assertEquals(1, context.getAllServices().size());
-        assertCommandIsNotRegistered(context, AddNoteCommand.NAME, AddNoteCommand.class);
-        assertCommandIsNotRegistered(context, DeleteNoteCommand.NAME, DeleteNoteCommand.class);
-        assertCommandIsNotRegistered(context, UpdateNoteCommand.NAME, UpdateNoteCommand.class);
-        assertCommandIsNotRegistered(context, ListNotesCommand.NAME, ListNotesCommand.class);
-        assertServiceIsRegistered(context, CompleterService.class, NoteIdCompleterService.class);
-    }
-
-}
--- a/notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/AddNoteCommandTest.java	Thu Nov 17 12:47:48 2016 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,203 +0,0 @@
-/*
- * Copyright 2012-2016 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.notes.client.cli.internal;
-
-import com.redhat.thermostat.client.cli.AgentArgument;
-import com.redhat.thermostat.client.cli.VmArgument;
-import com.redhat.thermostat.common.cli.Arguments;
-import com.redhat.thermostat.common.cli.CommandContext;
-import com.redhat.thermostat.common.cli.CommandException;
-import com.redhat.thermostat.notes.common.HostNote;
-import com.redhat.thermostat.notes.common.VmNote;
-import com.redhat.thermostat.storage.core.AgentId;
-import com.redhat.thermostat.storage.core.VmId;
-import com.redhat.thermostat.storage.model.AgentInformation;
-import com.redhat.thermostat.storage.model.VmInfo;
-import org.hamcrest.BaseMatcher;
-import org.hamcrest.Description;
-import org.hamcrest.Matcher;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-
-import java.util.UUID;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.junit.Assert.assertThat;
-import static org.junit.Assert.fail;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-
-public class AddNoteCommandTest extends AbstractNotesCommandTest<AddNoteCommand> {
-
-    @Override
-    public AddNoteCommand createCommand() {
-        return new AddNoteCommand();
-    }
-
-    @Test(expected = CommandException.class)
-    public void testRunFailsWithBothVmAndAgentIdGiven() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
-        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
-        CommandContext ctx = contextFactory.createContext(args);
-        command.run(ctx);
-    }
-
-    @Test(expected = CommandException.class)
-    public void testRunFailsWithNeitherVmNorAgentIdGiven() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(false);
-        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(false);
-        CommandContext ctx = contextFactory.createContext(args);
-        command.run(ctx);
-    }
-
-    @Test
-    public void testRunWithInvalidAgentId() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(AbstractNotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(AbstractNotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn("note content");
-        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(null);
-
-        doInvalidAgentIdTest(args);
-    }
-
-    @Test
-    public void testRunWithInvalidVmId() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(AbstractNotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(AbstractNotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn("note content");
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
-        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(null);
-
-        doInvalidVmIdTest(args);
-    }
-
-    @Test
-    public void testRunWithValidVmIdYieldingInvalidAgentId() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(AbstractNotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(AbstractNotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn("note content");
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
-
-        VmInfo vmInfo = new VmInfo();
-        vmInfo.setAgentId("foo-agentid");
-        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(null);
-
-        doInvalidAgentIdTest(args);
-    }
-
-    @Test
-    public void testRunWithHostNote() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
-        when(args.hasArgument(NotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(NotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn("note content");
-
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
-
-        CommandContext ctx = contextFactory.createContext(args);
-        ArgumentCaptor<HostNote> noteCaptor = ArgumentCaptor.forClass(HostNote.class);
-        command.run(ctx);
-        verifyZeroInteractions(vmNoteDAO);
-        verify(hostNoteDAO).add(noteCaptor.capture());
-        HostNote note = noteCaptor.getValue();
-        assertThat(note.getContent(), is("note content"));
-        assertThat(note.getAgentId(), is("foo-agentid"));
-        assertThat(note.getTimeStamp(), is(atLeast(0l)));
-        try {
-            UUID.fromString(note.getId());
-        } catch (Exception e) {
-            fail("Note ID should be a UUID, got: " + note.getId());
-        }
-    }
-
-    @Test
-    public void testRunWithVmNote() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
-        when(args.hasArgument(NotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(NotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn("note content");
-        VmInfo vmInfo = new VmInfo();
-        vmInfo.setAgentId("foo-agentid");
-        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
-        CommandContext ctx = contextFactory.createContext(args);
-        ArgumentCaptor<VmNote> noteCaptor = ArgumentCaptor.forClass(VmNote.class);
-        command.run(ctx);
-        verifyZeroInteractions(hostNoteDAO);
-        verify(vmNoteDAO).add(noteCaptor.capture());
-        VmNote note = noteCaptor.getValue();
-        assertThat(note.getContent(), is("note content"));
-        assertThat(note.getAgentId(), is("foo-agentid"));
-        assertThat(note.getVmId(), is("foo-vmid"));
-        assertThat(note.getTimeStamp(), is(atLeast(0l)));
-        try {
-            UUID.fromString(note.getId());
-        } catch (Exception e) {
-            fail("Note ID should be a UUID, got: " + note.getId());
-        }
-    }
-
-    private static Matcher<Long> atLeast(final Long l) {
-        return new BaseMatcher<Long>() {
-            @Override
-            public boolean matches(Object o) {
-                return o instanceof Long && ((long) o) >= l;
-            }
-
-            @Override
-            public void describeTo(Description description) {
-                description.appendText("long greater than or equal to ")
-                        .appendValue(l);
-            }
-        };
-    }
-
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/AddNoteSubcommandTest.java	Fri Oct 14 15:28:23 2016 -0400
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2012-2016 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.notes.client.cli.internal;
+
+import com.redhat.thermostat.client.cli.AgentArgument;
+import com.redhat.thermostat.client.cli.VmArgument;
+import com.redhat.thermostat.common.cli.Arguments;
+import com.redhat.thermostat.common.cli.CommandContext;
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.notes.common.HostNote;
+import com.redhat.thermostat.notes.common.VmNote;
+import com.redhat.thermostat.storage.core.AgentId;
+import com.redhat.thermostat.storage.core.VmId;
+import com.redhat.thermostat.storage.model.AgentInformation;
+import com.redhat.thermostat.storage.model.VmInfo;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.util.UUID;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class AddNoteSubcommandTest extends AbstractNotesCommandTest<AddNoteSubcommand> {
+
+    @Override
+    public AddNoteSubcommand createCommand() {
+        return new AddNoteSubcommand();
+    }
+
+    @Test(expected = CommandException.class)
+    public void testRunFailsWithBothVmAndAgentIdGiven() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
+        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
+        CommandContext ctx = contextFactory.createContext(args);
+        command.run(ctx);
+    }
+
+    @Test(expected = CommandException.class)
+    public void testRunFailsWithNeitherVmNorAgentIdGiven() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(false);
+        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(false);
+        CommandContext ctx = contextFactory.createContext(args);
+        command.run(ctx);
+    }
+
+    @Test
+    public void testRunWithInvalidAgentId() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn("note content");
+        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(null);
+
+        doInvalidAgentIdTest(args);
+    }
+
+    @Test
+    public void testRunWithInvalidVmId() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn("note content");
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
+        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(null);
+
+        doInvalidVmIdTest(args);
+    }
+
+    @Test
+    public void testRunWithValidVmIdYieldingInvalidAgentId() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn("note content");
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
+
+        VmInfo vmInfo = new VmInfo();
+        vmInfo.setAgentId("foo-agentid");
+        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(null);
+
+        doInvalidAgentIdTest(args);
+    }
+
+    @Test
+    public void testRunWithHostNote() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
+        when(args.hasArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn("note content");
+
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
+
+        CommandContext ctx = contextFactory.createContext(args);
+        ArgumentCaptor<HostNote> noteCaptor = ArgumentCaptor.forClass(HostNote.class);
+        command.run(ctx);
+        verifyZeroInteractions(vmNoteDAO);
+        verify(hostNoteDAO).add(noteCaptor.capture());
+        HostNote note = noteCaptor.getValue();
+        assertThat(note.getContent(), is("note content"));
+        assertThat(note.getAgentId(), is("foo-agentid"));
+        assertThat(note.getTimeStamp(), is(atLeast(0L)));
+        try {
+            UUID.fromString(note.getId());
+        } catch (Exception e) {
+            fail("Note ID should be a UUID, got: " + note.getId());
+        }
+    }
+
+    @Test
+    public void testRunWithVmNote() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
+        when(args.hasArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn("note content");
+        VmInfo vmInfo = new VmInfo();
+        vmInfo.setAgentId("foo-agentid");
+        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
+        CommandContext ctx = contextFactory.createContext(args);
+        ArgumentCaptor<VmNote> noteCaptor = ArgumentCaptor.forClass(VmNote.class);
+        command.run(ctx);
+        verifyZeroInteractions(hostNoteDAO);
+        verify(vmNoteDAO).add(noteCaptor.capture());
+        VmNote note = noteCaptor.getValue();
+        assertThat(note.getContent(), is("note content"));
+        assertThat(note.getAgentId(), is("foo-agentid"));
+        assertThat(note.getVmId(), is("foo-vmid"));
+        assertThat(note.getTimeStamp(), is(atLeast(0L)));
+        try {
+            UUID.fromString(note.getId());
+        } catch (Exception e) {
+            fail("Note ID should be a UUID, got: " + note.getId());
+        }
+    }
+
+    private static Matcher<Long> atLeast(final Long l) {
+        return new BaseMatcher<Long>() {
+            @Override
+            public boolean matches(Object o) {
+                return o instanceof Long && ((long) o) >= l;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("long greater than or equal to ")
+                        .appendValue(l);
+            }
+        };
+    }
+
+}
--- a/notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/DeleteNoteCommandTest.java	Thu Nov 17 12:47:48 2016 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,180 +0,0 @@
-/*
- * Copyright 2012-2016 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.notes.client.cli.internal;
-
-import com.redhat.thermostat.client.cli.AgentArgument;
-import com.redhat.thermostat.client.cli.VmArgument;
-import com.redhat.thermostat.common.cli.Arguments;
-import com.redhat.thermostat.common.cli.CommandContext;
-import com.redhat.thermostat.common.cli.CommandException;
-import com.redhat.thermostat.storage.core.AgentId;
-import com.redhat.thermostat.storage.core.HostRef;
-import com.redhat.thermostat.storage.core.VmId;
-import com.redhat.thermostat.storage.core.VmRef;
-import com.redhat.thermostat.storage.model.AgentInformation;
-import com.redhat.thermostat.storage.model.HostInfo;
-import com.redhat.thermostat.storage.model.VmInfo;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.junit.Assert.assertThat;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-
-public class DeleteNoteCommandTest extends AbstractNotesCommandTest<DeleteNoteCommand> {
-
-    @Override
-    public DeleteNoteCommand createCommand() {
-        return new DeleteNoteCommand();
-    }
-
-    @Test(expected = CommandException.class)
-    public void testRunFailsWithBothVmAndAgentIdGiven() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
-        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
-        CommandContext ctx = contextFactory.createContext(args);
-        command.run(ctx);
-    }
-
-    @Test(expected = CommandException.class)
-    public void testRunFailsWithNeitherVmNorAgentIdGiven() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(false);
-        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(false);
-        CommandContext ctx = contextFactory.createContext(args);
-        command.run(ctx);
-    }
-
-
-    @Test
-    public void testRunWithInvalidAgentId() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(AbstractNotesCommand.NOTE_ID_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(AbstractNotesCommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
-        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(null);
-
-        doInvalidAgentIdTest(args);
-    }
-
-    @Test
-    public void testRunWithInvalidVmId() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(AbstractNotesCommand.NOTE_ID_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(AbstractNotesCommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
-        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(null);
-
-        doInvalidVmIdTest(args);
-    }
-
-    @Test
-    public void testRunWithValidVmIdYieldingInvalidAgentId() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(AbstractNotesCommand.NOTE_ID_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(AbstractNotesCommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
-
-        VmInfo vmInfo = new VmInfo();
-        vmInfo.setAgentId("foo-agentid");
-        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(null);
-
-        doInvalidAgentIdTest(args);
-    }
-
-    @Test
-    public void testRunWithHostNote() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
-        when(args.hasArgument(NotesCommand.NOTE_ID_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(NotesCommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
-        HostInfo hostInfo = new HostInfo();
-        hostInfo.setHostname("foo-hostname");
-        hostInfo.setAgentId("foo-agentid");
-        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
-        CommandContext ctx = contextFactory.createContext(args);
-        command.run(ctx);
-        verifyZeroInteractions(vmNoteDAO);
-        ArgumentCaptor<HostRef> refCaptor = ArgumentCaptor.forClass(HostRef.class);
-        ArgumentCaptor<String> idCaptor = ArgumentCaptor.forClass(String.class);
-        verify(hostNoteDAO).removeById(refCaptor.capture(), idCaptor.capture());
-        assertThat(refCaptor.getValue().getAgentId(), is("foo-agentid"));
-        assertThat(idCaptor.getValue(), is("foo-noteid"));
-    }
-
-    @Test
-    public void testRunWithVmNote() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
-        when(args.hasArgument(NotesCommand.NOTE_ID_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(NotesCommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
-        HostInfo hostInfo = new HostInfo();
-        hostInfo.setHostname("foo-hostname");
-        hostInfo.setAgentId("foo-agentid");
-        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
-        VmInfo vmInfo = new VmInfo();
-        vmInfo.setAgentId("foo-agentid");
-        vmInfo.setVmPid(100);
-        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
-        CommandContext ctx = contextFactory.createContext(args);
-        command.run(ctx);
-        verifyZeroInteractions(hostNoteDAO);
-        ArgumentCaptor<VmRef> refCaptor = ArgumentCaptor.forClass(VmRef.class);
-        ArgumentCaptor<String> idCaptor = ArgumentCaptor.forClass(String.class);
-        verify(vmNoteDAO).removeById(refCaptor.capture(), idCaptor.capture());
-        assertThat(refCaptor.getValue().getHostRef().getAgentId(), is("foo-agentid"));
-        assertThat(refCaptor.getValue().getVmId(), is("foo-vmid"));
-        assertThat(refCaptor.getValue().getPid(), is(100));
-        assertThat(idCaptor.getValue(), is("foo-noteid"));
-    }
-
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/DeleteNoteSubcommandTest.java	Fri Oct 14 15:28:23 2016 -0400
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2012-2016 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.notes.client.cli.internal;
+
+import com.redhat.thermostat.client.cli.AgentArgument;
+import com.redhat.thermostat.client.cli.VmArgument;
+import com.redhat.thermostat.common.cli.Arguments;
+import com.redhat.thermostat.common.cli.CommandContext;
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.storage.core.AgentId;
+import com.redhat.thermostat.storage.core.HostRef;
+import com.redhat.thermostat.storage.core.VmId;
+import com.redhat.thermostat.storage.core.VmRef;
+import com.redhat.thermostat.storage.model.AgentInformation;
+import com.redhat.thermostat.storage.model.HostInfo;
+import com.redhat.thermostat.storage.model.VmInfo;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class DeleteNoteSubcommandTest extends AbstractNotesCommandTest<DeleteNoteSubcommand> {
+
+    @Override
+    public DeleteNoteSubcommand createCommand() {
+        return new DeleteNoteSubcommand();
+    }
+
+    @Test(expected = CommandException.class)
+    public void testRunFailsWithBothVmAndAgentIdGiven() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
+        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
+        CommandContext ctx = contextFactory.createContext(args);
+        command.run(ctx);
+    }
+
+    @Test(expected = CommandException.class)
+    public void testRunFailsWithNeitherVmNorAgentIdGiven() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(false);
+        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(false);
+        CommandContext ctx = contextFactory.createContext(args);
+        command.run(ctx);
+    }
+
+
+    @Test
+    public void testRunWithInvalidAgentId() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
+        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(null);
+
+        doInvalidAgentIdTest(args);
+    }
+
+    @Test
+    public void testRunWithInvalidVmId() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
+        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(null);
+
+        doInvalidVmIdTest(args);
+    }
+
+    @Test
+    public void testRunWithValidVmIdYieldingInvalidAgentId() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
+
+        VmInfo vmInfo = new VmInfo();
+        vmInfo.setAgentId("foo-agentid");
+        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(null);
+
+        doInvalidAgentIdTest(args);
+    }
+
+    @Test
+    public void testRunWithHostNote() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
+        when(args.hasArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
+        HostInfo hostInfo = new HostInfo();
+        hostInfo.setHostname("foo-hostname");
+        hostInfo.setAgentId("foo-agentid");
+        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
+        CommandContext ctx = contextFactory.createContext(args);
+        command.run(ctx);
+        verifyZeroInteractions(vmNoteDAO);
+        ArgumentCaptor<HostRef> refCaptor = ArgumentCaptor.forClass(HostRef.class);
+        ArgumentCaptor<String> idCaptor = ArgumentCaptor.forClass(String.class);
+        verify(hostNoteDAO).removeById(refCaptor.capture(), idCaptor.capture());
+        assertThat(refCaptor.getValue().getAgentId(), is("foo-agentid"));
+        assertThat(idCaptor.getValue(), is("foo-noteid"));
+    }
+
+    @Test
+    public void testRunWithVmNote() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
+        when(args.hasArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
+        HostInfo hostInfo = new HostInfo();
+        hostInfo.setHostname("foo-hostname");
+        hostInfo.setAgentId("foo-agentid");
+        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
+        VmInfo vmInfo = new VmInfo();
+        vmInfo.setAgentId("foo-agentid");
+        vmInfo.setVmPid(100);
+        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
+        CommandContext ctx = contextFactory.createContext(args);
+        command.run(ctx);
+        verifyZeroInteractions(hostNoteDAO);
+        ArgumentCaptor<VmRef> refCaptor = ArgumentCaptor.forClass(VmRef.class);
+        ArgumentCaptor<String> idCaptor = ArgumentCaptor.forClass(String.class);
+        verify(vmNoteDAO).removeById(refCaptor.capture(), idCaptor.capture());
+        assertThat(refCaptor.getValue().getHostRef().getAgentId(), is("foo-agentid"));
+        assertThat(refCaptor.getValue().getVmId(), is("foo-vmid"));
+        assertThat(refCaptor.getValue().getPid(), is(100));
+        assertThat(idCaptor.getValue(), is("foo-noteid"));
+    }
+
+}
--- a/notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/ListNotesCommandTest.java	Thu Nov 17 12:47:48 2016 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,320 +0,0 @@
-/*
- * Copyright 2012-2016 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.notes.client.cli.internal;
-
-import com.redhat.thermostat.client.cli.AgentArgument;
-import com.redhat.thermostat.client.cli.VmArgument;
-import com.redhat.thermostat.common.Clock;
-import com.redhat.thermostat.common.cli.Arguments;
-import com.redhat.thermostat.common.cli.CommandContext;
-import com.redhat.thermostat.common.cli.CommandException;
-import com.redhat.thermostat.notes.common.HostNote;
-import com.redhat.thermostat.notes.common.VmNote;
-import com.redhat.thermostat.storage.core.AgentId;
-import com.redhat.thermostat.storage.core.HostRef;
-import com.redhat.thermostat.storage.core.VmId;
-import com.redhat.thermostat.storage.core.VmRef;
-import com.redhat.thermostat.storage.model.AgentInformation;
-import com.redhat.thermostat.storage.model.HostInfo;
-import com.redhat.thermostat.storage.model.VmInfo;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-
-import java.util.Arrays;
-import java.util.Date;
-import java.util.UUID;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.not;
-import static org.junit.Assert.assertThat;
-import static org.junit.matchers.JUnitMatchers.containsString;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-
-public class ListNotesCommandTest extends AbstractNotesCommandTest<ListNotesCommand> {
-
-    @Override
-    public ListNotesCommand createCommand() {
-        return new ListNotesCommand();
-    }
-
-    @Test(expected = CommandException.class)
-    public void testRunFailsWithBothVmAndAgentIdGiven() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
-        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
-        CommandContext ctx = contextFactory.createContext(args);
-        command.run(ctx);
-    }
-
-    @Test(expected = CommandException.class)
-    public void testRunFailsWithNeitherVmNorAgentIdGiven() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(false);
-        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(false);
-        CommandContext ctx = contextFactory.createContext(args);
-        command.run(ctx);
-    }
-
-
-    @Test
-    public void testRunWithInvalidAgentId() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(null);
-
-        doInvalidAgentIdTest(args);
-    }
-
-    @Test
-    public void testRunWithInvalidVmId() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
-        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(null);
-
-        doInvalidVmIdTest(args);
-    }
-
-    @Test
-    public void testRunWithValidVmIdYieldingInvalidAgentId() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
-
-        VmInfo vmInfo = new VmInfo();
-        vmInfo.setAgentId("foo-agentid");
-        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(null);
-
-        doInvalidAgentIdTest(args);
-    }
-
-    @Test
-    public void testRunWithHostAndNoNotes() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
-        HostInfo hostInfo = new HostInfo();
-        hostInfo.setHostname("foo-hostname");
-        hostInfo.setAgentId("foo-agentid");
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
-        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
-        CommandContext ctx = contextFactory.createContext(args);
-        command.run(ctx);
-        verifyZeroInteractions(vmNoteDAO);
-        ArgumentCaptor<HostRef> refCaptor = ArgumentCaptor.forClass(HostRef.class);
-        verify(hostNoteDAO).getFor(refCaptor.capture());
-        assertThat(refCaptor.getValue().getAgentId(), is("foo-agentid"));
-        assertThat(contextFactory.getOutput(), is(""));
-    }
-
-    @Test
-    public void testRunWithHostWithNotes() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
-        HostInfo hostInfo = new HostInfo();
-        hostInfo.setHostname("foo-hostname");
-        hostInfo.setAgentId("foo-agentid");
-        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
-        HostNote note1 = mock(HostNote.class);
-        when(note1.getContent()).thenReturn("note 1");
-        when(note1.getTimeStamp()).thenReturn(100l);
-        when(note1.getId()).thenReturn(UUID.randomUUID().toString());
-        HostNote note2 = mock(HostNote.class);
-        when(note2.getContent()).thenReturn("note 2");
-        when(note2.getTimeStamp()).thenReturn(100l);
-        when(note2.getId()).thenReturn(UUID.randomUUID().toString());
-        when(hostNoteDAO.getFor(any(HostRef.class))).thenReturn(Arrays.asList(note1, note2));
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
-        CommandContext ctx = contextFactory.createContext(args);
-        command.run(ctx);
-        verifyZeroInteractions(vmNoteDAO);
-        ArgumentCaptor<HostRef> refCaptor = ArgumentCaptor.forClass(HostRef.class);
-        verify(hostNoteDAO).getFor(refCaptor.capture());
-        assertThat(refCaptor.getValue().getAgentId(), is("foo-agentid"));
-        String printed = contextFactory.getOutput();
-        assertThat(printed, containsString(note1.getContent()));
-        assertThat(printed, containsString(Clock.DEFAULT_DATE_FORMAT.format(new Date(note1.getTimeStamp()))));
-        assertThat(printed, not(containsString(note1.getId())));
-        assertThat(printed, containsString(note2.getContent()));
-        assertThat(printed, containsString(Clock.DEFAULT_DATE_FORMAT.format(new Date(note2.getTimeStamp()))));
-        assertThat(printed, not(containsString(note2.getId())));
-    }
-
-    @Test
-    public void testRunWithHostWithNotesAndShowIdOption() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
-        when(args.hasArgument(ListNotesCommand.SHOW_NOTE_ID_ARGUMENT)).thenReturn(true);
-        HostInfo hostInfo = new HostInfo();
-        hostInfo.setHostname("foo-hostname");
-        hostInfo.setAgentId("foo-agentid");
-        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
-        HostNote note1 = mock(HostNote.class);
-        when(note1.getContent()).thenReturn("note 1");
-        when(note1.getTimeStamp()).thenReturn(100l);
-        when(note1.getId()).thenReturn(UUID.randomUUID().toString());
-        HostNote note2 = mock(HostNote.class);
-        when(note2.getContent()).thenReturn("note 2");
-        when(note2.getTimeStamp()).thenReturn(100l);
-        when(note2.getId()).thenReturn(UUID.randomUUID().toString());
-        when(hostNoteDAO.getFor(any(HostRef.class))).thenReturn(Arrays.asList(note1, note2));
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
-        CommandContext ctx = contextFactory.createContext(args);
-        command.run(ctx);
-        verifyZeroInteractions(vmNoteDAO);
-        ArgumentCaptor<HostRef> refCaptor = ArgumentCaptor.forClass(HostRef.class);
-        verify(hostNoteDAO).getFor(refCaptor.capture());
-        assertThat(refCaptor.getValue().getAgentId(), is("foo-agentid"));
-        String printed = contextFactory.getOutput();
-        assertThat(printed, containsString(note1.getContent()));
-        assertThat(printed, containsString(Clock.DEFAULT_DATE_FORMAT.format(new Date(note1.getTimeStamp()))));
-        assertThat(printed, containsString(note1.getId()));
-        assertThat(printed, containsString(note2.getContent()));
-        assertThat(printed, containsString(Clock.DEFAULT_DATE_FORMAT.format(new Date(note2.getTimeStamp()))));
-        assertThat(printed, containsString(note2.getId()));
-    }
-
-    @Test
-    public void testRunWithHostWithNotesAndQuietOption() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
-        when(args.hasArgument(ListNotesCommand.QUIET_ARGUMENT)).thenReturn(true);
-        HostInfo hostInfo = new HostInfo();
-        hostInfo.setHostname("foo-hostname");
-        hostInfo.setAgentId("foo-agentid");
-        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
-        HostNote note1 = mock(HostNote.class);
-        when(note1.getContent()).thenReturn("note 1");
-        when(note1.getTimeStamp()).thenReturn(100l);
-        when(note1.getId()).thenReturn(UUID.randomUUID().toString());
-        HostNote note2 = mock(HostNote.class);
-        when(note2.getContent()).thenReturn("note 2");
-        when(note2.getTimeStamp()).thenReturn(100l);
-        when(note2.getId()).thenReturn(UUID.randomUUID().toString());
-        when(hostNoteDAO.getFor(any(HostRef.class))).thenReturn(Arrays.asList(note1, note2));
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
-        CommandContext ctx = contextFactory.createContext(args);
-        command.run(ctx);
-        verifyZeroInteractions(vmNoteDAO);
-        ArgumentCaptor<HostRef> refCaptor = ArgumentCaptor.forClass(HostRef.class);
-        verify(hostNoteDAO).getFor(refCaptor.capture());
-        assertThat(refCaptor.getValue().getAgentId(), is("foo-agentid"));
-        String printed = contextFactory.getOutput();
-        assertThat(printed, not(containsString(note1.getContent())));
-        assertThat(printed, not(containsString(Clock.DEFAULT_DATE_FORMAT.format(new Date(note1.getTimeStamp())))));
-        assertThat(printed, containsString(note1.getId()));
-        assertThat(printed, not(containsString(note2.getContent())));
-        assertThat(printed, not(containsString(Clock.DEFAULT_DATE_FORMAT.format(new Date(note2.getTimeStamp())))));
-        assertThat(printed, containsString(note2.getId()));
-    }
-
-    @Test
-    public void testRunWithVmAndNoNotes() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
-        HostInfo hostInfo = new HostInfo();
-        hostInfo.setHostname("foo-hostname");
-        hostInfo.setAgentId("foo-agentid");
-        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
-        VmInfo vmInfo = new VmInfo();
-        vmInfo.setAgentId("foo-agentid");
-        vmInfo.setVmPid(100);
-        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
-        CommandContext ctx = contextFactory.createContext(args);
-        command.run(ctx);
-        verifyZeroInteractions(hostNoteDAO);
-        ArgumentCaptor<VmRef> refCaptor = ArgumentCaptor.forClass(VmRef.class);
-        verify(vmNoteDAO).getFor(refCaptor.capture());
-        assertThat(refCaptor.getValue().getHostRef().getAgentId(), is("foo-agentid"));
-        assertThat(refCaptor.getValue().getVmId(), is("foo-vmid"));
-        assertThat(refCaptor.getValue().getPid(), is(100));
-    }
-
-    @Test
-    public void testRunWithVmWithNotes() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
-        HostInfo hostInfo = new HostInfo();
-        hostInfo.setHostname("foo-hostname");
-        hostInfo.setAgentId("foo-agentid");
-        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
-        VmNote note1 = mock(VmNote.class);
-        when(note1.getContent()).thenReturn("note 1");
-        when(note1.getTimeStamp()).thenReturn(100l);
-        when(note1.getId()).thenReturn(UUID.randomUUID().toString());
-        VmNote note2 = mock(VmNote.class);
-        when(note2.getContent()).thenReturn("note 2");
-        when(note2.getTimeStamp()).thenReturn(100l);
-        when(note2.getId()).thenReturn(UUID.randomUUID().toString());
-        when(vmNoteDAO.getFor(any(VmRef.class))).thenReturn(Arrays.asList(note1, note2));
-        VmInfo vmInfo = new VmInfo();
-        vmInfo.setAgentId("foo-agentid");
-        vmInfo.setVmPid(100);
-        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
-        CommandContext ctx = contextFactory.createContext(args);
-        command.run(ctx);
-        verifyZeroInteractions(hostNoteDAO);
-        ArgumentCaptor<VmRef> refCaptor = ArgumentCaptor.forClass(VmRef.class);
-        verify(vmNoteDAO).getFor(refCaptor.capture());
-        assertThat(refCaptor.getValue().getHostRef().getAgentId(), is("foo-agentid"));
-        assertThat(refCaptor.getValue().getVmId(), is("foo-vmid"));
-        assertThat(refCaptor.getValue().getPid(), is(100));
-        String printed = contextFactory.getOutput();
-        assertThat(printed, containsString(note1.getContent()));
-        assertThat(printed, containsString(Clock.DEFAULT_DATE_FORMAT.format(new Date(note1.getTimeStamp()))));
-        assertThat(printed, not(containsString(note1.getId())));
-        assertThat(printed, containsString(note2.getContent()));
-        assertThat(printed, containsString(Clock.DEFAULT_DATE_FORMAT.format(new Date(note2.getTimeStamp()))));
-        assertThat(printed, not(containsString(note2.getId())));
-    }
-
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/ListNotesSubcommandTest.java	Fri Oct 14 15:28:23 2016 -0400
@@ -0,0 +1,320 @@
+/*
+ * Copyright 2012-2016 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.notes.client.cli.internal;
+
+import com.redhat.thermostat.client.cli.AgentArgument;
+import com.redhat.thermostat.client.cli.VmArgument;
+import com.redhat.thermostat.common.Clock;
+import com.redhat.thermostat.common.cli.Arguments;
+import com.redhat.thermostat.common.cli.CommandContext;
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.notes.common.HostNote;
+import com.redhat.thermostat.notes.common.VmNote;
+import com.redhat.thermostat.storage.core.AgentId;
+import com.redhat.thermostat.storage.core.HostRef;
+import com.redhat.thermostat.storage.core.VmId;
+import com.redhat.thermostat.storage.core.VmRef;
+import com.redhat.thermostat.storage.model.AgentInformation;
+import com.redhat.thermostat.storage.model.HostInfo;
+import com.redhat.thermostat.storage.model.VmInfo;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.UUID;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertThat;
+import static org.junit.matchers.JUnitMatchers.containsString;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class ListNotesSubcommandTest extends AbstractNotesCommandTest<ListNotesSubcommand> {
+
+    @Override
+    public ListNotesSubcommand createCommand() {
+        return new ListNotesSubcommand();
+    }
+
+    @Test(expected = CommandException.class)
+    public void testRunFailsWithBothVmAndAgentIdGiven() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
+        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
+        CommandContext ctx = contextFactory.createContext(args);
+        command.run(ctx);
+    }
+
+    @Test(expected = CommandException.class)
+    public void testRunFailsWithNeitherVmNorAgentIdGiven() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(false);
+        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(false);
+        CommandContext ctx = contextFactory.createContext(args);
+        command.run(ctx);
+    }
+
+
+    @Test
+    public void testRunWithInvalidAgentId() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(null);
+
+        doInvalidAgentIdTest(args);
+    }
+
+    @Test
+    public void testRunWithInvalidVmId() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
+        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(null);
+
+        doInvalidVmIdTest(args);
+    }
+
+    @Test
+    public void testRunWithValidVmIdYieldingInvalidAgentId() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
+
+        VmInfo vmInfo = new VmInfo();
+        vmInfo.setAgentId("foo-agentid");
+        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(null);
+
+        doInvalidAgentIdTest(args);
+    }
+
+    @Test
+    public void testRunWithHostAndNoNotes() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
+        HostInfo hostInfo = new HostInfo();
+        hostInfo.setHostname("foo-hostname");
+        hostInfo.setAgentId("foo-agentid");
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
+        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
+        CommandContext ctx = contextFactory.createContext(args);
+        command.run(ctx);
+        verifyZeroInteractions(vmNoteDAO);
+        ArgumentCaptor<HostRef> refCaptor = ArgumentCaptor.forClass(HostRef.class);
+        verify(hostNoteDAO).getFor(refCaptor.capture());
+        assertThat(refCaptor.getValue().getAgentId(), is("foo-agentid"));
+        assertThat(contextFactory.getOutput(), is(""));
+    }
+
+    @Test
+    public void testRunWithHostWithNotes() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
+        HostInfo hostInfo = new HostInfo();
+        hostInfo.setHostname("foo-hostname");
+        hostInfo.setAgentId("foo-agentid");
+        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
+        HostNote note1 = mock(HostNote.class);
+        when(note1.getContent()).thenReturn("note 1");
+        when(note1.getTimeStamp()).thenReturn(100L);
+        when(note1.getId()).thenReturn(UUID.randomUUID().toString());
+        HostNote note2 = mock(HostNote.class);
+        when(note2.getContent()).thenReturn("note 2");
+        when(note2.getTimeStamp()).thenReturn(100L);
+        when(note2.getId()).thenReturn(UUID.randomUUID().toString());
+        when(hostNoteDAO.getFor(any(HostRef.class))).thenReturn(Arrays.asList(note1, note2));
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
+        CommandContext ctx = contextFactory.createContext(args);
+        command.run(ctx);
+        verifyZeroInteractions(vmNoteDAO);
+        ArgumentCaptor<HostRef> refCaptor = ArgumentCaptor.forClass(HostRef.class);
+        verify(hostNoteDAO).getFor(refCaptor.capture());
+        assertThat(refCaptor.getValue().getAgentId(), is("foo-agentid"));
+        String printed = contextFactory.getOutput();
+        assertThat(printed, containsString(note1.getContent()));
+        assertThat(printed, containsString(Clock.DEFAULT_DATE_FORMAT.format(new Date(note1.getTimeStamp()))));
+        assertThat(printed, not(containsString(note1.getId())));
+        assertThat(printed, containsString(note2.getContent()));
+        assertThat(printed, containsString(Clock.DEFAULT_DATE_FORMAT.format(new Date(note2.getTimeStamp()))));
+        assertThat(printed, not(containsString(note2.getId())));
+    }
+
+    @Test
+    public void testRunWithHostWithNotesAndShowIdOption() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
+        when(args.hasArgument(ListNotesSubcommand.SHOW_NOTE_ID_ARGUMENT)).thenReturn(true);
+        HostInfo hostInfo = new HostInfo();
+        hostInfo.setHostname("foo-hostname");
+        hostInfo.setAgentId("foo-agentid");
+        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
+        HostNote note1 = mock(HostNote.class);
+        when(note1.getContent()).thenReturn("note 1");
+        when(note1.getTimeStamp()).thenReturn(100L);
+        when(note1.getId()).thenReturn(UUID.randomUUID().toString());
+        HostNote note2 = mock(HostNote.class);
+        when(note2.getContent()).thenReturn("note 2");
+        when(note2.getTimeStamp()).thenReturn(100L);
+        when(note2.getId()).thenReturn(UUID.randomUUID().toString());
+        when(hostNoteDAO.getFor(any(HostRef.class))).thenReturn(Arrays.asList(note1, note2));
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
+        CommandContext ctx = contextFactory.createContext(args);
+        command.run(ctx);
+        verifyZeroInteractions(vmNoteDAO);
+        ArgumentCaptor<HostRef> refCaptor = ArgumentCaptor.forClass(HostRef.class);
+        verify(hostNoteDAO).getFor(refCaptor.capture());
+        assertThat(refCaptor.getValue().getAgentId(), is("foo-agentid"));
+        String printed = contextFactory.getOutput();
+        assertThat(printed, containsString(note1.getContent()));
+        assertThat(printed, containsString(Clock.DEFAULT_DATE_FORMAT.format(new Date(note1.getTimeStamp()))));
+        assertThat(printed, containsString(note1.getId()));
+        assertThat(printed, containsString(note2.getContent()));
+        assertThat(printed, containsString(Clock.DEFAULT_DATE_FORMAT.format(new Date(note2.getTimeStamp()))));
+        assertThat(printed, containsString(note2.getId()));
+    }
+
+    @Test
+    public void testRunWithHostWithNotesAndQuietOption() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
+        when(args.hasArgument(ListNotesSubcommand.QUIET_ARGUMENT)).thenReturn(true);
+        HostInfo hostInfo = new HostInfo();
+        hostInfo.setHostname("foo-hostname");
+        hostInfo.setAgentId("foo-agentid");
+        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
+        HostNote note1 = mock(HostNote.class);
+        when(note1.getContent()).thenReturn("note 1");
+        when(note1.getTimeStamp()).thenReturn(100L);
+        when(note1.getId()).thenReturn(UUID.randomUUID().toString());
+        HostNote note2 = mock(HostNote.class);
+        when(note2.getContent()).thenReturn("note 2");
+        when(note2.getTimeStamp()).thenReturn(100L);
+        when(note2.getId()).thenReturn(UUID.randomUUID().toString());
+        when(hostNoteDAO.getFor(any(HostRef.class))).thenReturn(Arrays.asList(note1, note2));
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
+        CommandContext ctx = contextFactory.createContext(args);
+        command.run(ctx);
+        verifyZeroInteractions(vmNoteDAO);
+        ArgumentCaptor<HostRef> refCaptor = ArgumentCaptor.forClass(HostRef.class);
+        verify(hostNoteDAO).getFor(refCaptor.capture());
+        assertThat(refCaptor.getValue().getAgentId(), is("foo-agentid"));
+        String printed = contextFactory.getOutput();
+        assertThat(printed, not(containsString(note1.getContent())));
+        assertThat(printed, not(containsString(Clock.DEFAULT_DATE_FORMAT.format(new Date(note1.getTimeStamp())))));
+        assertThat(printed, containsString(note1.getId()));
+        assertThat(printed, not(containsString(note2.getContent())));
+        assertThat(printed, not(containsString(Clock.DEFAULT_DATE_FORMAT.format(new Date(note2.getTimeStamp())))));
+        assertThat(printed, containsString(note2.getId()));
+    }
+
+    @Test
+    public void testRunWithVmAndNoNotes() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
+        HostInfo hostInfo = new HostInfo();
+        hostInfo.setHostname("foo-hostname");
+        hostInfo.setAgentId("foo-agentid");
+        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
+        VmInfo vmInfo = new VmInfo();
+        vmInfo.setAgentId("foo-agentid");
+        vmInfo.setVmPid(100);
+        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
+        CommandContext ctx = contextFactory.createContext(args);
+        command.run(ctx);
+        verifyZeroInteractions(hostNoteDAO);
+        ArgumentCaptor<VmRef> refCaptor = ArgumentCaptor.forClass(VmRef.class);
+        verify(vmNoteDAO).getFor(refCaptor.capture());
+        assertThat(refCaptor.getValue().getHostRef().getAgentId(), is("foo-agentid"));
+        assertThat(refCaptor.getValue().getVmId(), is("foo-vmid"));
+        assertThat(refCaptor.getValue().getPid(), is(100));
+    }
+
+    @Test
+    public void testRunWithVmWithNotes() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
+        HostInfo hostInfo = new HostInfo();
+        hostInfo.setHostname("foo-hostname");
+        hostInfo.setAgentId("foo-agentid");
+        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
+        VmNote note1 = mock(VmNote.class);
+        when(note1.getContent()).thenReturn("note 1");
+        when(note1.getTimeStamp()).thenReturn(100L);
+        when(note1.getId()).thenReturn(UUID.randomUUID().toString());
+        VmNote note2 = mock(VmNote.class);
+        when(note2.getContent()).thenReturn("note 2");
+        when(note2.getTimeStamp()).thenReturn(100L);
+        when(note2.getId()).thenReturn(UUID.randomUUID().toString());
+        when(vmNoteDAO.getFor(any(VmRef.class))).thenReturn(Arrays.asList(note1, note2));
+        VmInfo vmInfo = new VmInfo();
+        vmInfo.setAgentId("foo-agentid");
+        vmInfo.setVmPid(100);
+        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
+        CommandContext ctx = contextFactory.createContext(args);
+        command.run(ctx);
+        verifyZeroInteractions(hostNoteDAO);
+        ArgumentCaptor<VmRef> refCaptor = ArgumentCaptor.forClass(VmRef.class);
+        verify(vmNoteDAO).getFor(refCaptor.capture());
+        assertThat(refCaptor.getValue().getHostRef().getAgentId(), is("foo-agentid"));
+        assertThat(refCaptor.getValue().getVmId(), is("foo-vmid"));
+        assertThat(refCaptor.getValue().getPid(), is(100));
+        String printed = contextFactory.getOutput();
+        assertThat(printed, containsString(note1.getContent()));
+        assertThat(printed, containsString(Clock.DEFAULT_DATE_FORMAT.format(new Date(note1.getTimeStamp()))));
+        assertThat(printed, not(containsString(note1.getId())));
+        assertThat(printed, containsString(note2.getContent()));
+        assertThat(printed, containsString(Clock.DEFAULT_DATE_FORMAT.format(new Date(note2.getTimeStamp()))));
+        assertThat(printed, not(containsString(note2.getId())));
+    }
+
+}
--- a/notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/NoteIdCompleterServiceTest.java	Thu Nov 17 12:47:48 2016 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,120 +0,0 @@
-/*
- * Copyright 2012-2016 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.notes.client.cli.internal;
-
-import com.redhat.thermostat.common.cli.CliCommandOption;
-import com.redhat.thermostat.common.cli.TabCompleter;
-import com.redhat.thermostat.notes.common.HostNoteDAO;
-import com.redhat.thermostat.notes.common.VmNoteDAO;
-import org.hamcrest.BaseMatcher;
-import org.hamcrest.Description;
-import org.hamcrest.Matcher;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-import static org.hamcrest.CoreMatchers.equalTo;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.not;
-import static org.hamcrest.CoreMatchers.nullValue;
-import static org.junit.Assert.assertThat;
-import static org.mockito.Mockito.mock;
-
-public class NoteIdCompleterServiceTest {
-
-    private HostNoteDAO hostDao;
-    private VmNoteDAO vmDao;
-    private NoteIdCompleterService service;
-
-    @Before
-    public void setup() {
-        hostDao = mock(HostNoteDAO.class);
-        vmDao = mock(VmNoteDAO.class);
-        service = new NoteIdCompleterService();
-        service.setHostNoteDao(hostDao);
-        service.setVmNoteDao(vmDao);
-    }
-
-    @Test
-    public void testGetCommands() {
-        List<String> commands = new ArrayList<>(service.getCommands());
-        List<String> expected = new ArrayList<>(Arrays.asList(
-                DeleteNoteCommand.NAME,
-                UpdateNoteCommand.NAME
-        ));
-        Collections.sort(commands);
-        Collections.sort(expected);
-        assertThat(commands, is(equalTo(expected)));
-    }
-
-    @Test
-    public void testGetOptionCompleters() {
-        Map<CliCommandOption, ? extends TabCompleter> map = service.getOptionCompleters();
-        assertThat(map.size(), is(1));
-        for (Map.Entry<CliCommandOption, ? extends TabCompleter> entry : map.entrySet()) {
-            CliCommandOption opt = entry.getKey();
-            assertThat(opt.getOpt(), is("n"));
-            assertThat(opt.getLongOpt(), is("noteId"));
-            assertThat(opt.hasArg(), is(true));
-            assertThat(opt.isRequired(), is(false));
-            assertThat(opt.getDescription(), is(not(nullValue())));
-            assertThat(opt.getDescription().length(), is(atLeast(1)));
-        }
-    }
-
-    private static Matcher<Integer> atLeast(final Integer i) {
-        return new BaseMatcher<Integer>() {
-            @Override
-            public boolean matches(Object o) {
-                return o instanceof Integer && ((int) o) >= i;
-            }
-
-            @Override
-            public void describeTo(Description description) {
-                description.appendText("int greater than or equal to ")
-                        .appendValue(i);
-            }
-        };
-    }
-
-
-}
--- a/notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/NoteIdsFinderTest.java	Thu Nov 17 12:47:48 2016 -0500
+++ b/notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/NoteIdsFinderTest.java	Fri Oct 14 15:28:23 2016 -0400
@@ -37,7 +37,6 @@
 package com.redhat.thermostat.notes.client.cli.internal;
 
 import com.redhat.thermostat.common.cli.CompletionInfo;
-import com.redhat.thermostat.common.cli.DependencyServices;
 import com.redhat.thermostat.notes.common.HostNote;
 import com.redhat.thermostat.notes.common.HostNoteDAO;
 import com.redhat.thermostat.notes.common.Note;
@@ -62,7 +61,6 @@
 
 public class NoteIdsFinderTest {
 
-    private DependencyServices dependencyServices;
     private HostNoteDAO hostDao;
     private VmNoteDAO vmDao;
     private NoteIdsFinder finder;
@@ -73,13 +71,8 @@
 
     @Before
     public void setup() {
-        dependencyServices = mock(DependencyServices.class);
         hostDao = mock(HostNoteDAO.class);
         vmDao = mock(VmNoteDAO.class);
-        when(dependencyServices.hasService(HostNoteDAO.class)).thenReturn(true);
-        when(dependencyServices.getService(HostNoteDAO.class)).thenReturn(hostDao);
-        when(dependencyServices.hasService(VmNoteDAO.class)).thenReturn(true);
-        when(dependencyServices.getService(VmNoteDAO.class)).thenReturn(vmDao);
 
         hostNote1 = new HostNote();
         hostNote1.setId("host-note-id-01");
@@ -111,7 +104,9 @@
 
         when(vmDao.getAll()).thenReturn(Arrays.asList(vmNote1, vmNote2));
 
-        finder = new NoteIdsFinder(dependencyServices);
+        finder = new NoteIdsFinder();
+        finder.bindVmNoteDao(vmDao);
+        finder.bindHostNoteDao(hostDao);
     }
 
     @Test
@@ -230,9 +225,4 @@
         return result;
     }
 
-    @Test
-    public void testListDependencies() {
-        assertThat(finder.getRequiredDependencies(), is(equalTo(new Class[]{ HostNoteDAO.class, VmNoteDAO.class })));
-    }
-
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/NotesControlCommandTest.java	Fri Oct 14 15:28:23 2016 -0400
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2012-2016 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.notes.client.cli.internal;
+
+import com.redhat.thermostat.common.cli.Arguments;
+import com.redhat.thermostat.common.cli.CliCommandOption;
+import com.redhat.thermostat.common.cli.CommandContext;
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.common.cli.TabCompleter;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class NotesControlCommandTest {
+
+    private AddNoteSubcommand addNoteCommand;
+    private DeleteNoteSubcommand deleteNoteCommand;
+    private ListNotesSubcommand listNotesCommand;
+    private UpdateNoteSubcommand updateNoteCommand;
+    private NoteIdsFinder noteIdsFinder;
+
+    private NotesControlCommand notesControlCommand;
+
+    @Before
+    public void setup() {
+        addNoteCommand = mock(AddNoteSubcommand.class);
+        deleteNoteCommand = mock(DeleteNoteSubcommand.class);
+        listNotesCommand = mock(ListNotesSubcommand.class);
+        updateNoteCommand = mock(UpdateNoteSubcommand.class);
+        noteIdsFinder = mock(NoteIdsFinder.class);
+
+        notesControlCommand = new NotesControlCommand(noteIdsFinder, addNoteCommand, deleteNoteCommand, updateNoteCommand,
+                listNotesCommand);
+    }
+
+    @Test
+    public void testCommandName() {
+        assertThat(NotesControlCommand.COMMAND_NAME, is("notes"));
+    }
+
+    @Test
+    public void testSubcommandCompleters() {
+        Map<String, Map<CliCommandOption, ? extends TabCompleter>> map = notesControlCommand.getSubcommandCompleters();
+
+        assertThat(map.size(), is(2));
+        assertThat(new HashSet<>(map.keySet()),
+                is(equalTo(new HashSet<>(Arrays.asList(UpdateNoteSubcommand.SUBCOMMAND_NAME, DeleteNoteSubcommand.SUBCOMMAND_NAME)))));
+        Map<CliCommandOption, ? extends TabCompleter> completer = map.get(UpdateNoteSubcommand.SUBCOMMAND_NAME);
+        assertThat(completer.size(), is(1));
+        assertThat(completer.keySet(), is(equalTo(Collections.singleton(NotesControlCommand.NOTE_ID_OPTION))));
+        assertThat(completer.get(NotesControlCommand.NOTE_ID_OPTION), is(not(equalTo(null))));
+    }
+
+    @Test
+    public void testNoteIdOption() {
+        assertThat(NotesControlCommand.NOTE_ID_OPTION.getOpt(), is("n"));
+        assertThat(NotesControlCommand.NOTE_ID_OPTION.getLongOpt(), is("noteId"));
+    }
+
+    @Test
+    public void verifyAddNoteSubcommandDelegation() throws CommandException {
+        performSubcommandTest(AddNoteSubcommand.SUBCOMMAND_NAME, addNoteCommand);
+    }
+
+    @Test
+    public void verifyDeleteNoteSubcommandDelegation() throws CommandException {
+        performSubcommandTest(DeleteNoteSubcommand.SUBCOMMAND_NAME, deleteNoteCommand);
+    }
+
+    @Test
+    public void verifyListNotesSubcommandDelegation() throws CommandException {
+        performSubcommandTest(ListNotesSubcommand.SUBCOMMAND_NAME, listNotesCommand);
+    }
+
+    @Test
+    public void verifyUpdateNoteSubcommandDelegation() throws CommandException {
+        performSubcommandTest(UpdateNoteSubcommand.SUBCOMMAND_NAME, updateNoteCommand);
+    }
+
+    private void performSubcommandTest(String subcommandName, NotesSubcommand subcommand) throws CommandException {
+        CommandContext ctx = mock(CommandContext.class);
+        Arguments args = mock(Arguments.class);
+        when(ctx.getArguments()).thenReturn(args);
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList(subcommandName));
+
+        notesControlCommand.run(ctx);
+        verify(subcommand).run(ctx);
+    }
+
+    @Test(expected = CommandException.class)
+    public void testUnknownSubcommand() throws CommandException {
+        CommandContext ctx = mock(CommandContext.class);
+        Arguments args = mock(Arguments.class);
+        when(ctx.getArguments()).thenReturn(args);
+        when(args.getNonOptionArguments()).thenReturn(Collections.singletonList("fake-subcommand"));
+
+        notesControlCommand.run(ctx);
+    }
+
+    @Test(expected = CommandException.class)
+    public void testNoSubcommand() throws CommandException {
+        CommandContext ctx = mock(CommandContext.class);
+        Arguments args = mock(Arguments.class);
+        when(ctx.getArguments()).thenReturn(args);
+        when(args.getNonOptionArguments()).thenReturn(Collections.<String>emptyList());
+
+        notesControlCommand.run(ctx);
+    }
+
+    @Test
+    public void verifyNoteIdsFinderIsPassed() {
+        Map<String, Map<CliCommandOption, ? extends TabCompleter>> map = notesControlCommand.getSubcommandCompleters();
+        map.get(DeleteNoteSubcommand.SUBCOMMAND_NAME).get(NotesControlCommand.NOTE_ID_OPTION).complete("", 0, new ArrayList<CharSequence>());
+        verify(noteIdsFinder).findCompletions();
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testOptionCompletersIsEmpty() {
+        assertThat(notesControlCommand.getOptionCompleters().size(), is(0));
+    }
+
+}
--- a/notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/UpdateNoteCommandTest.java	Thu Nov 17 12:47:48 2016 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,269 +0,0 @@
-/*
- * Copyright 2012-2016 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.notes.client.cli.internal;
-
-import com.redhat.thermostat.client.cli.AgentArgument;
-import com.redhat.thermostat.client.cli.VmArgument;
-import com.redhat.thermostat.common.cli.Arguments;
-import com.redhat.thermostat.common.cli.CommandContext;
-import com.redhat.thermostat.common.cli.CommandException;
-import com.redhat.thermostat.notes.common.HostNote;
-import com.redhat.thermostat.notes.common.VmNote;
-import com.redhat.thermostat.storage.core.AgentId;
-import com.redhat.thermostat.storage.core.HostRef;
-import com.redhat.thermostat.storage.core.VmId;
-import com.redhat.thermostat.storage.core.VmRef;
-import com.redhat.thermostat.storage.model.AgentInformation;
-import com.redhat.thermostat.storage.model.HostInfo;
-import com.redhat.thermostat.storage.model.VmInfo;
-import org.hamcrest.BaseMatcher;
-import org.hamcrest.Description;
-import org.hamcrest.Matcher;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.junit.Assert.assertThat;
-import static org.junit.Assert.fail;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-
-public class UpdateNoteCommandTest extends AbstractNotesCommandTest<UpdateNoteCommand> {
-
-    @Override
-    public UpdateNoteCommand createCommand() {
-        return new UpdateNoteCommand();
-    }
-
-    @Test(expected = CommandException.class)
-    public void testRunFailsWithBothVmAndAgentIdGiven() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
-        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
-        CommandContext ctx = contextFactory.createContext(args);
-        command.run(ctx);
-    }
-
-    @Test(expected = CommandException.class)
-    public void testRunFailsWithNeitherVmNorAgentIdGiven() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(false);
-        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(false);
-        CommandContext ctx = contextFactory.createContext(args);
-        command.run(ctx);
-    }
-
-    @Test
-    public void testRunWithInvalidAgentId() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(AbstractNotesCommand.NOTE_ID_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(AbstractNotesCommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
-        when(args.hasArgument(AbstractNotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(AbstractNotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn("note content");
-        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(null);
-
-        doInvalidAgentIdTest(args);
-    }
-
-    @Test
-    public void testRunWithInvalidVmId() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(AbstractNotesCommand.NOTE_ID_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(AbstractNotesCommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
-        when(args.hasArgument(AbstractNotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(AbstractNotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn("note content");
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
-        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(null);
-
-        doInvalidVmIdTest(args);
-    }
-
-    @Test
-    public void testRunWithNoMatchingHostNoteFromStorage() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(AbstractNotesCommand.NOTE_ID_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(AbstractNotesCommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
-        when(args.hasArgument(AbstractNotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(AbstractNotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn("note content");
-        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
-        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(mock(HostInfo.class));
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(mock(AgentInformation.class));
-        when(hostNoteDAO.getById(any(HostRef.class), any(String.class))).thenReturn(null);
-
-        CommandContext ctx = contextFactory.createContext(args);
-        try {
-            command.run(ctx);
-            fail();
-        } catch (CommandException ex) {
-            // pass
-        }
-    }
-
-    @Test
-    public void testRunWithNoMatchingVmNoteFromStorage() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(AbstractNotesCommand.NOTE_ID_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(AbstractNotesCommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
-        when(args.hasArgument(AbstractNotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(AbstractNotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn("note content");
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
-        VmInfo vmInfo = new VmInfo();
-        vmInfo.setAgentId("foo-agentid");
-        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
-        when(vmNoteDAO.getById(any(VmRef.class), any(String.class))).thenReturn(null);
-
-        CommandContext ctx = contextFactory.createContext(args);
-        try {
-            command.run(ctx);
-            fail();
-        } catch (CommandException ex) {
-            // pass
-        }
-    }
-
-    @Test
-    public void testRunWithValidVmIdYieldingInvalidAgentId() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(AbstractNotesCommand.NOTE_ID_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(AbstractNotesCommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
-        when(args.hasArgument(AbstractNotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(AbstractNotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn("note content");
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
-
-        VmInfo vmInfo = new VmInfo();
-        vmInfo.setAgentId("foo-agentid");
-        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(null);
-
-        doInvalidAgentIdTest(args);
-    }
-
-    @Test
-    public void testRunWithHostNote() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
-        when(args.hasArgument(NotesCommand.NOTE_ID_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(NotesCommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
-        when(args.hasArgument(NotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(NotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn("new note content");
-        HostNote oldNote = new HostNote();
-        oldNote.setId("foo-noteid");
-        oldNote.setAgentId("foo-agentid");
-        oldNote.setTimeStamp(100l);
-        oldNote.setContent("old note content");
-        when(hostNoteDAO.getById(any(HostRef.class), eq("foo-noteid"))).thenReturn(oldNote);
-        HostInfo hostInfo = new HostInfo();
-        hostInfo.setHostname("foo-hostname");
-        hostInfo.setAgentId("foo-agentid");
-        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
-        CommandContext ctx = contextFactory.createContext(args);
-        ArgumentCaptor<HostNote> noteCaptor = ArgumentCaptor.forClass(HostNote.class);
-        command.run(ctx);
-        verifyZeroInteractions(vmNoteDAO);
-        verify(hostNoteDAO).update(noteCaptor.capture());
-        HostNote note = noteCaptor.getValue();
-        assertThat(note.getContent(), is("new note content"));
-        assertThat(note.getAgentId(), is(oldNote.getAgentId()));
-        assertThat(note.getId(), is(oldNote.getId()));
-        assertThat(note.getTimeStamp(), is(atLeast(oldNote.getTimeStamp())));
-    }
-
-    @Test
-    public void testRunWithVmNote() throws CommandException {
-        Arguments args = mock(Arguments.class);
-        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
-        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
-        when(args.hasArgument(NotesCommand.NOTE_ID_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(NotesCommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
-        when(args.hasArgument(NotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
-        when(args.getArgument(NotesCommand.NOTE_CONTENT_ARGUMENT)).thenReturn("new note content");
-        VmNote oldNote = new VmNote();
-        oldNote.setId("foo-noteid");
-        oldNote.setAgentId("foo-agentid");
-        oldNote.setTimeStamp(100l);
-        oldNote.setContent("old note content");
-        when(vmNoteDAO.getById(any(VmRef.class), eq("foo-noteid"))).thenReturn(oldNote);
-        HostInfo hostInfo = new HostInfo();
-        hostInfo.setHostname("foo-hostname");
-        hostInfo.setAgentId("foo-agentid");
-        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
-        VmInfo vmInfo = new VmInfo();
-        vmInfo.setAgentId("foo-agentid");
-        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
-        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
-        CommandContext ctx = contextFactory.createContext(args);
-        ArgumentCaptor<VmNote> noteCaptor = ArgumentCaptor.forClass(VmNote.class);
-        command.run(ctx);
-        verifyZeroInteractions(hostNoteDAO);
-        verify(vmNoteDAO).update(noteCaptor.capture());
-        VmNote note = noteCaptor.getValue();
-        assertThat(note.getContent(), is("new note content"));
-        assertThat(note.getAgentId(), is(oldNote.getAgentId()));
-        assertThat(note.getId(), is(oldNote.getId()));
-        assertThat(note.getTimeStamp(), is(atLeast(oldNote.getTimeStamp())));
-    }
-
-    private static Matcher<Long> atLeast(final Long l) {
-        return new BaseMatcher<Long>() {
-            @Override
-            public boolean matches(Object o) {
-                return o instanceof Long && ((long) o) >= l;
-            }
-
-            @Override
-            public void describeTo(Description description) {
-                description.appendText("long greater than or equal to ")
-                        .appendValue(l);
-            }
-        };
-    }
-
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/notes/client-cli/src/test/java/com/redhat/thermostat/notes/client/cli/internal/UpdateNoteSubcommandTest.java	Fri Oct 14 15:28:23 2016 -0400
@@ -0,0 +1,269 @@
+/*
+ * Copyright 2012-2016 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.notes.client.cli.internal;
+
+import com.redhat.thermostat.client.cli.AgentArgument;
+import com.redhat.thermostat.client.cli.VmArgument;
+import com.redhat.thermostat.common.cli.Arguments;
+import com.redhat.thermostat.common.cli.CommandContext;
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.notes.common.HostNote;
+import com.redhat.thermostat.notes.common.VmNote;
+import com.redhat.thermostat.storage.core.AgentId;
+import com.redhat.thermostat.storage.core.HostRef;
+import com.redhat.thermostat.storage.core.VmId;
+import com.redhat.thermostat.storage.core.VmRef;
+import com.redhat.thermostat.storage.model.AgentInformation;
+import com.redhat.thermostat.storage.model.HostInfo;
+import com.redhat.thermostat.storage.model.VmInfo;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class UpdateNoteSubcommandTest extends AbstractNotesCommandTest<UpdateNoteSubcommand> {
+
+    @Override
+    public UpdateNoteSubcommand createCommand() {
+        return new UpdateNoteSubcommand();
+    }
+
+    @Test(expected = CommandException.class)
+    public void testRunFailsWithBothVmAndAgentIdGiven() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
+        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
+        CommandContext ctx = contextFactory.createContext(args);
+        command.run(ctx);
+    }
+
+    @Test(expected = CommandException.class)
+    public void testRunFailsWithNeitherVmNorAgentIdGiven() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(false);
+        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(false);
+        CommandContext ctx = contextFactory.createContext(args);
+        command.run(ctx);
+    }
+
+    @Test
+    public void testRunWithInvalidAgentId() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
+        when(args.hasArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn("note content");
+        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(null);
+
+        doInvalidAgentIdTest(args);
+    }
+
+    @Test
+    public void testRunWithInvalidVmId() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
+        when(args.hasArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn("note content");
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
+        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(null);
+
+        doInvalidVmIdTest(args);
+    }
+
+    @Test
+    public void testRunWithNoMatchingHostNoteFromStorage() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
+        when(args.hasArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn("note content");
+        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
+        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(mock(HostInfo.class));
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(mock(AgentInformation.class));
+        when(hostNoteDAO.getById(any(HostRef.class), any(String.class))).thenReturn(null);
+
+        CommandContext ctx = contextFactory.createContext(args);
+        try {
+            command.run(ctx);
+            fail();
+        } catch (CommandException ex) {
+            // pass
+        }
+    }
+
+    @Test
+    public void testRunWithNoMatchingVmNoteFromStorage() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
+        when(args.hasArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn("note content");
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
+        VmInfo vmInfo = new VmInfo();
+        vmInfo.setAgentId("foo-agentid");
+        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
+        when(vmNoteDAO.getById(any(VmRef.class), any(String.class))).thenReturn(null);
+
+        CommandContext ctx = contextFactory.createContext(args);
+        try {
+            command.run(ctx);
+            fail();
+        } catch (CommandException ex) {
+            // pass
+        }
+    }
+
+    @Test
+    public void testRunWithValidVmIdYieldingInvalidAgentId() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
+        when(args.hasArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn("note content");
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
+
+        VmInfo vmInfo = new VmInfo();
+        vmInfo.setAgentId("foo-agentid");
+        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(null);
+
+        doInvalidAgentIdTest(args);
+    }
+
+    @Test
+    public void testRunWithHostNote() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(AgentArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(AgentArgument.ARGUMENT_NAME)).thenReturn("foo-agentid");
+        when(args.hasArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
+        when(args.hasArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn("new note content");
+        HostNote oldNote = new HostNote();
+        oldNote.setId("foo-noteid");
+        oldNote.setAgentId("foo-agentid");
+        oldNote.setTimeStamp(100L);
+        oldNote.setContent("old note content");
+        when(hostNoteDAO.getById(any(HostRef.class), eq("foo-noteid"))).thenReturn(oldNote);
+        HostInfo hostInfo = new HostInfo();
+        hostInfo.setHostname("foo-hostname");
+        hostInfo.setAgentId("foo-agentid");
+        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
+        CommandContext ctx = contextFactory.createContext(args);
+        ArgumentCaptor<HostNote> noteCaptor = ArgumentCaptor.forClass(HostNote.class);
+        command.run(ctx);
+        verifyZeroInteractions(vmNoteDAO);
+        verify(hostNoteDAO).update(noteCaptor.capture());
+        HostNote note = noteCaptor.getValue();
+        assertThat(note.getContent(), is("new note content"));
+        assertThat(note.getAgentId(), is(oldNote.getAgentId()));
+        assertThat(note.getId(), is(oldNote.getId()));
+        assertThat(note.getTimeStamp(), is(atLeast(oldNote.getTimeStamp())));
+    }
+
+    @Test
+    public void testRunWithVmNote() throws CommandException {
+        Arguments args = mock(Arguments.class);
+        when(args.hasArgument(VmArgument.ARGUMENT_NAME)).thenReturn(true);
+        when(args.getArgument(VmArgument.ARGUMENT_NAME)).thenReturn("foo-vmid");
+        when(args.hasArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_ID_ARGUMENT)).thenReturn("foo-noteid");
+        when(args.hasArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn(true);
+        when(args.getArgument(NotesSubcommand.NOTE_CONTENT_ARGUMENT)).thenReturn("new note content");
+        VmNote oldNote = new VmNote();
+        oldNote.setId("foo-noteid");
+        oldNote.setAgentId("foo-agentid");
+        oldNote.setTimeStamp(100L);
+        oldNote.setContent("old note content");
+        when(vmNoteDAO.getById(any(VmRef.class), eq("foo-noteid"))).thenReturn(oldNote);
+        HostInfo hostInfo = new HostInfo();
+        hostInfo.setHostname("foo-hostname");
+        hostInfo.setAgentId("foo-agentid");
+        when(hostInfoDAO.getHostInfo(any(AgentId.class))).thenReturn(hostInfo);
+        VmInfo vmInfo = new VmInfo();
+        vmInfo.setAgentId("foo-agentid");
+        when(vmInfoDAO.getVmInfo(any(VmId.class))).thenReturn(vmInfo);
+        when(agentInfoDAO.getAgentInformation(any(AgentId.class))).thenReturn(new AgentInformation());
+        CommandContext ctx = contextFactory.createContext(args);
+        ArgumentCaptor<VmNote> noteCaptor = ArgumentCaptor.forClass(VmNote.class);
+        command.run(ctx);
+        verifyZeroInteractions(hostNoteDAO);
+        verify(vmNoteDAO).update(noteCaptor.capture());
+        VmNote note = noteCaptor.getValue();
+        assertThat(note.getContent(), is("new note content"));
+        assertThat(note.getAgentId(), is(oldNote.getAgentId()));
+        assertThat(note.getId(), is(oldNote.getId()));
+        assertThat(note.getTimeStamp(), is(atLeast(oldNote.getTimeStamp())));
+    }
+
+    private static Matcher<Long> atLeast(final Long l) {
+        return new BaseMatcher<Long>() {
+            @Override
+            public boolean matches(Object o) {
+                return o instanceof Long && ((long) o) >= l;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("long greater than or equal to ")
+                        .appendValue(l);
+            }
+        };
+    }
+
+}
--- a/notes/distribution/thermostat-plugin.xml	Thu Nov 17 12:47:48 2016 -0500
+++ b/notes/distribution/thermostat-plugin.xml	Fri Oct 14 15:28:23 2016 -0400
@@ -41,21 +41,80 @@
   xsi:schemaLocation="http://icedtea.classpath.org/thermostat/plugins/v1.0 thermostat-plugin.xsd">
   <commands>
     <command>
-      <name>list-notes</name>
-      <summary>list notes for the specified Host or VM</summary>
-      <description>List notes for the specified Host or VM.</description>
+      <name>notes</name>
+      <summary>manipulate notes on Hosts or VMs</summary>
+      <description>Add, remove, update, and list notes for Hosts and VMs</description>
+
+      <subcommands>
+        <subcommand>
+          <name>list</name>
+          <description>List notes for the specified Host or VM.</description>
+          <options>
+            <option>
+              <long>show-note-id</long>
+              <required>false</required>
+              <description>show note IDs</description>
+            </option>
+            <option>
+              <long>quiet</long>
+              <required>false</required>
+              <description>only show Note IDs</description>
+            </option>
+          </options>
+        </subcommand>
+
+        <subcommand>
+          <name>add</name>
+          <description>Add a note to the specified Host or VM.</description>
+          <options>
+            <option>
+              <long>content</long>
+              <short>c</short>
+              <argument>string</argument>
+              <required>true</required>
+              <description>the note content string</description>
+            </option>
+          </options>
+        </subcommand>
+
+        <subcommand>
+          <name>delete</name>
+          <description>Delete a Host or VM Note.</description>
+          <options>
+            <option>
+              <long>noteId</long>
+              <short>n</short>
+              <argument>id</argument>
+              <required>true</required>
+              <description>the note ID</description>
+            </option>
+          </options>
+        </subcommand>
+
+        <subcommand>
+          <name>update</name>
+          <description>Update a Host or VM Note.</description>
+          <options>
+            <option>
+              <long>noteId</long>
+              <short>n</short>
+              <argument>id</argument>
+              <required>true</required>
+              <description>the note ID</description>
+            </option>
+            <option>
+              <long>content</long>
+              <short>c</short>
+              <argument>string</argument>
+              <required>true</required>
+              <description>the note content string</description>
+            </option>
+          </options>
+        </subcommand>
+      </subcommands>
+
       <options>
         <option>
-          <long>show-note-id</long>
-          <required>false</required>
-          <description>show note IDs</description>
-        </option>
-        <option>
-          <long>quiet</long>
-          <required>false</required>
-          <description>only show Note IDs</description>
-        </option>
-        <option>
           <long>vmId</long>
           <short>v</short>
           <argument>vm</argument>
@@ -76,57 +135,12 @@
           <long>logLevel</long>
         </option>
       </options>
+
       <environments>
         <environment>cli</environment>
         <environment>shell</environment>
       </environments>
-      <bundles>
-        <bundle><symbolic-name>com.redhat.thermostat.notes.common</symbolic-name><version>${project.version}</version></bundle>
-        <bundle><symbolic-name>com.redhat.thermostat.notes.client.cli</symbolic-name><version>${project.version}</version></bundle>
-        <bundle><symbolic-name>com.redhat.thermostat.storage.mongodb</symbolic-name><version>${project.version}</version></bundle>
-        <bundle><symbolic-name>com.redhat.thermostat.web.common</symbolic-name><version>${project.version}</version></bundle>
-        <bundle><symbolic-name>com.redhat.thermostat.web.client</symbolic-name><version>${project.version}</version></bundle>
-      </bundles>
-    </command>
 
-    <command>
-      <name>add-note</name>
-      <summary>add a note to a host or VM</summary>
-      <description>Add a note to the specified Host or VM.</description>
-      <options>
-        <option>
-          <long>vmId</long>
-          <short>v</short>
-          <argument>vm</argument>
-          <required>false</required>
-          <description>the VM to add a note to</description>
-        </option>
-        <option>
-          <long>agentId</long>
-          <short>a</short>
-          <argument>agent</argument>
-          <required>false</required>
-          <description>the agent (host) to add a note to</description>
-        </option>
-        <option>
-          <long>content</long>
-          <short>c</short>
-          <argument>string</argument>
-          <required>false</required>
-          <description>the note content string. If not provided then the non-option arguments are assumed to be the
-              note content</description>
-        </option>
-        <option common="true">
-          <long>dbUrl</long>
-        </option>
-        <option common="true">
-          <long>logLevel</long>
-        </option>
-      </options>
-      <environments>
-        <environment>cli</environment>
-        <environment>shell</environment>
-      </environments>
       <bundles>
         <bundle><symbolic-name>com.redhat.thermostat.notes.common</symbolic-name><version>${project.version}</version></bundle>
         <bundle><symbolic-name>com.redhat.thermostat.notes.client.cli</symbolic-name><version>${project.version}</version></bundle>
@@ -134,108 +148,8 @@
         <bundle><symbolic-name>com.redhat.thermostat.web.common</symbolic-name><version>${project.version}</version></bundle>
         <bundle><symbolic-name>com.redhat.thermostat.web.client</symbolic-name><version>${project.version}</version></bundle>
       </bundles>
-    </command>
 
-    <command>
-      <name>delete-note</name>
-      <summary>delete a note from a host or VM</summary>
-      <description>Delete a Host or VM Note.</description>
-      <options>
-        <option>
-          <long>vmId</long>
-          <short>v</short>
-          <argument>vm</argument>
-          <required>false</required>
-          <description>the VM to add a note to</description>
-        </option>
-        <option>
-          <long>agentId</long>
-          <short>a</short>
-          <argument>agent</argument>
-          <required>false</required>
-          <description>the agent (host) to add a note to</description>
-        </option>
-        <option>
-          <long>noteId</long>
-          <short>n</short>
-          <argument>id</argument>
-          <required>true</required>
-          <description>the note ID</description>
-        </option>
-        <option common="true">
-          <long>dbUrl</long>
-        </option>
-        <option common="true">
-          <long>logLevel</long>
-        </option>
-      </options>
-      <environments>
-        <environment>cli</environment>
-        <environment>shell</environment>
-      </environments>
-      <bundles>
-        <bundle><symbolic-name>com.redhat.thermostat.notes.common</symbolic-name><version>${project.version}</version></bundle>
-        <bundle><symbolic-name>com.redhat.thermostat.notes.client.cli</symbolic-name><version>${project.version}</version></bundle>
-        <bundle><symbolic-name>com.redhat.thermostat.storage.mongodb</symbolic-name><version>${project.version}</version></bundle>
-        <bundle><symbolic-name>com.redhat.thermostat.web.common</symbolic-name><version>${project.version}</version></bundle>
-        <bundle><symbolic-name>com.redhat.thermostat.web.client</symbolic-name><version>${project.version}</version></bundle>
-      </bundles>
     </command>
-
-    <command>
-      <name>update-note</name>
-      <summary>update a note on a host or VM</summary>
-      <description>Update a Host or VM Note.</description>
-      <options>
-        <option>
-          <long>vmId</long>
-          <short>v</short>
-          <argument>vm</argument>
-          <required>false</required>
-          <description>the ID of the VM of the note to be updated</description>
-        </option>
-        <option>
-          <long>agentId</long>
-          <short>a</short>
-          <argument>agent</argument>
-          <required>false</required>
-          <description>the ID of the agent (host) of the note to be updated</description>
-        </option>
-        <option>
-          <long>noteId</long>
-          <short>n</short>
-          <argument>id</argument>
-          <required>true</required>
-          <description>the note ID</description>
-        </option>
-        <option>
-          <long>content</long>
-          <short>c</short>
-          <argument>string</argument>
-          <required>false</required>
-          <description>the note content string. If not provided then the non-option arguments are assumed to be the
-              note content</description>
-        </option>
-        <option common="true">
-          <long>dbUrl</long>
-        </option>
-        <option common="true">
-          <long>logLevel</long>
-        </option>
-      </options>
-      <environments>
-        <environment>cli</environment>
-        <environment>shell</environment>
-      </environments>
-      <bundles>
-        <bundle><symbolic-name>com.redhat.thermostat.notes.common</symbolic-name><version>${project.version}</version></bundle>
-        <bundle><symbolic-name>com.redhat.thermostat.notes.client.cli</symbolic-name><version>${project.version}</version></bundle>
-        <bundle><symbolic-name>com.redhat.thermostat.storage.mongodb</symbolic-name><version>${project.version}</version></bundle>
-        <bundle><symbolic-name>com.redhat.thermostat.web.common</symbolic-name><version>${project.version}</version></bundle>
-        <bundle><symbolic-name>com.redhat.thermostat.web.client</symbolic-name><version>${project.version}</version></bundle>
-      </bundles>
-    </command>
-
   </commands>
   <extensions>
     <extension>