changeset 1227:19edbf121251

Use where expressions for Replace. Reviewed-by: omajid Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-August/008004.html
author Severin Gehwolf <sgehwolf@redhat.com>
date Thu, 22 Aug 2013 15:05:29 +0200
parents 278053e5c52c
children b4637cac2cbc
files storage/core/src/main/java/com/redhat/thermostat/storage/core/QueuedStorage.java storage/core/src/main/java/com/redhat/thermostat/storage/core/Replace.java storage/core/src/main/java/com/redhat/thermostat/storage/internal/dao/NetworkInterfaceInfoDAOImpl.java storage/core/src/test/java/com/redhat/thermostat/storage/core/QueuedStorageTest.java storage/core/src/test/java/com/redhat/thermostat/storage/internal/dao/NetworkInterfaceInfoDAOTest.java storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/MongoStorage.java storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoStorageTest.java thread/collector/src/main/java/com/redhat/thermostat/thread/dao/impl/ThreadDaoImpl.java thread/collector/src/test/java/com/redhat/thermostat/thread/dao/impl/ThreadDaoImplTest.java web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorage.java web/client/src/test/java/com/redhat/thermostat/web/client/internal/WebStorageTest.java web/common/src/main/java/com/redhat/thermostat/web/common/WebAdd.java web/common/src/main/java/com/redhat/thermostat/web/common/WebInsert.java web/common/src/main/java/com/redhat/thermostat/web/common/WebReplace.java web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java
diffstat 16 files changed, 456 insertions(+), 142 deletions(-) [+]
line wrap: on
line diff
--- a/storage/core/src/main/java/com/redhat/thermostat/storage/core/QueuedStorage.java	Sun Aug 25 18:32:31 2013 -0600
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/core/QueuedStorage.java	Thu Aug 22 15:05:29 2013 +0200
@@ -38,6 +38,7 @@
 package com.redhat.thermostat.storage.core;
 
 import java.io.InputStream;
+import java.util.Objects;
 import java.util.UUID;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -52,13 +53,20 @@
 
     private class QueuedReplace extends BasePut implements Replace {
 
+        private Expression expression;
+        
         private QueuedReplace(Category<?> category) {
             super(category);
         }
         
         @Override
         public void apply() {
-            replaceImpl(getCategory(), getPojo());
+            replaceImpl(getCategory(), getPojo(), expression);
+        }
+
+        @Override
+        public void where(Expression expression) {
+            this.expression = Objects.requireNonNull(expression);
         }
         
     }
@@ -151,7 +159,7 @@
         return replace;
     }
 
-    private void replaceImpl(final Category<?> category, final Pojo pojo) {
+    private void replaceImpl(final Category<?> category, final Pojo pojo, final Expression expression) {
         
         executor.execute(new Runnable() {
             
@@ -159,6 +167,7 @@
             public void run() {
                 Replace replace = delegate.createReplace(category);
                 replace.setPojo(pojo);
+                replace.where(expression);
                 replace.apply();
             }
 
--- a/storage/core/src/main/java/com/redhat/thermostat/storage/core/Replace.java	Sun Aug 25 18:32:31 2013 -0600
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/core/Replace.java	Thu Aug 22 15:05:29 2013 +0200
@@ -37,7 +37,19 @@
 
 package com.redhat.thermostat.storage.core;
 
+import com.redhat.thermostat.storage.query.Expression;
+
+/**
+ * Describes which object should get inserted into storage or which
+ * object should get updated with new values in storage.
+ * 
+ * If the where expression matches an object in storage, this
+ * object will get updated. Otherwise a new object gets inserted into
+ * storage.
+ *
+ */
 public interface Replace extends Put {
 
+    void where(Expression expression);
 }
 
--- a/storage/core/src/main/java/com/redhat/thermostat/storage/internal/dao/NetworkInterfaceInfoDAOImpl.java	Sun Aug 25 18:32:31 2013 -0600
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/internal/dao/NetworkInterfaceInfoDAOImpl.java	Thu Aug 22 15:05:29 2013 +0200
@@ -54,6 +54,8 @@
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.dao.NetworkInterfaceInfoDAO;
 import com.redhat.thermostat.storage.model.NetworkInterfaceInfo;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 
 public class NetworkInterfaceInfoDAOImpl implements NetworkInterfaceInfoDAO {
 
@@ -99,7 +101,16 @@
     @Override
     public void putNetworkInterfaceInfo(NetworkInterfaceInfo info) {
         Replace replace = storage.createReplace(networkInfoCategory);
+        ExpressionFactory factory = new ExpressionFactory();
+        String agentId = info.getAgentId();
+        if (agentId == null) {
+            agentId = storage.getAgentId();
+        }
+        Expression left = factory.equalTo(Key.AGENT_ID, agentId);
+        Expression right = factory.equalTo(NetworkInterfaceInfoDAO.ifaceKey, info.getInterfaceName());
+        Expression expression = factory.and(left, right); 
         replace.setPojo(info);
+        replace.where(expression);
         replace.apply();
     }
 
--- a/storage/core/src/test/java/com/redhat/thermostat/storage/core/QueuedStorageTest.java	Sun Aug 25 18:32:31 2013 -0600
+++ b/storage/core/src/test/java/com/redhat/thermostat/storage/core/QueuedStorageTest.java	Thu Aug 22 15:05:29 2013 +0200
@@ -69,6 +69,8 @@
 import org.junit.Test;
 
 import com.redhat.thermostat.storage.model.Pojo;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 
 
 public class QueuedStorageTest {
@@ -233,12 +235,14 @@
     }
 
     @Test
-    public void testInsert() {
+    public void testReplace() {
         Category<?> category = mock(Category.class);
         Pojo pojo = mock(Pojo.class);
 
-        Put put = queuedStorage.createReplace(category);
+        Replace put = queuedStorage.createReplace(category);
         put.setPojo(pojo);
+        Expression expression = new ExpressionFactory().equalTo(Key.AGENT_ID, "foo");
+        put.where(expression);
         put.apply();
 
         Runnable r = executor.getTask();
@@ -249,6 +253,7 @@
         r.run();
         verify(delegateStorage).createReplace(category);
         verify(delegateReplace).setPojo(pojo);
+        verify(delegateReplace).where(expression);
         verify(delegateReplace).apply();
         verifyNoMoreInteractions(delegateStorage);
 
--- a/storage/core/src/test/java/com/redhat/thermostat/storage/internal/dao/NetworkInterfaceInfoDAOTest.java	Sun Aug 25 18:32:31 2013 -0600
+++ b/storage/core/src/test/java/com/redhat/thermostat/storage/internal/dao/NetworkInterfaceInfoDAOTest.java	Thu Aug 22 15:05:29 2013 +0200
@@ -37,6 +37,7 @@
 package com.redhat.thermostat.storage.internal.dao;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.mock;
@@ -61,6 +62,8 @@
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.dao.NetworkInterfaceInfoDAO;
 import com.redhat.thermostat.storage.model.NetworkInterfaceInfo;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 
 public class NetworkInterfaceInfoDAOTest {
 
@@ -132,19 +135,46 @@
 
     @Test
     public void testPutNetworkInterfaceInfo() {
+        String agentId = "fooAgent";
+        doTestPutNetworkInerfaceInfo(false, agentId);
+    }
+    
+    @Test
+    public void testPutNetworkInterfaceInfoWithoutAgentIdInInfo() {
+        String agentId = "fooStorageAgentId";
+        doTestPutNetworkInerfaceInfo(true, agentId);
+    }
+    
+    private void doTestPutNetworkInerfaceInfo(boolean agentIdFromStorage, String agentId) {
         Storage storage = mock(Storage.class);
         Replace replace = mock(Replace.class);
         when(storage.createReplace(any(Category.class))).thenReturn(replace);
+        if (agentIdFromStorage) {
+            when(storage.getAgentId()).thenReturn(agentId);
+        }
 
         NetworkInterfaceInfo info = new NetworkInterfaceInfo(INTERFACE_NAME);
         info.setIp4Addr(IPV4_ADDR);
         info.setIp6Addr(IPV6_ADDR);
+        if (!agentIdFromStorage) {
+            info.setAgentId(agentId);
+        } else {
+            // case where agentId gets replaced by the DAO
+            // with the one set for storage.
+            assertNull(info.getAgentId());
+        }
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression left = factory.equalTo(Key.AGENT_ID, agentId);
+        Expression right = factory.equalTo(NetworkInterfaceInfoDAO.ifaceKey, INTERFACE_NAME);
+        Expression expected = factory.and(left, right);
         NetworkInterfaceInfoDAO dao = new NetworkInterfaceInfoDAOImpl(storage);
         dao.putNetworkInterfaceInfo(info);
 
         verify(storage).createReplace(NetworkInterfaceInfoDAO.networkInfoCategory);
         verify(replace).setPojo(info);
+        verify(replace).where(expected);
         verify(replace).apply();
     }
+    
 }
 
--- a/storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/MongoStorage.java	Sun Aug 25 18:32:31 2013 -0600
+++ b/storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/MongoStorage.java	Thu Aug 22 15:05:29 2013 +0200
@@ -37,10 +37,10 @@
 package com.redhat.thermostat.storage.mongodb.internal;
 
 import java.io.InputStream;
-import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.UUID;
 import java.util.concurrent.CountDownLatch;
 
@@ -114,14 +114,28 @@
     }
 
     private class MongoReplace extends BasePut implements Replace {
+        
+        private DBObject query;
+        private final MongoExpressionParser parser;
 
         private MongoReplace(Category<?> category) {
             super(category);
+            this.parser = new MongoExpressionParser();
         }
         
         @Override
         public void apply() {
-            replaceImpl(getCategory(), getPojo());
+            if (query == null) {
+                String msg = "where expression must be set. " +
+                             "Please call where() before apply().";
+                throw new IllegalStateException(msg);
+            }
+            replaceImpl(getCategory(), getPojo(), query);
+        }
+
+        @Override
+        public void where(Expression expression) {
+            this.query = parser.parse(Objects.requireNonNull(expression));
         }
         
     }
@@ -231,18 +245,9 @@
         coll.insert(toInsert);
     }
 
-    private void replaceImpl(final Category<?> cat, final Pojo pojo) {
+    private void replaceImpl(final Category<?> cat, final Pojo pojo, final DBObject query) {
         DBCollection coll = getCachedCollection(cat);
         DBObject toInsert = preparePut(pojo);
-
-        DBObject query = new BasicDBObject();
-        Collection<Key<?>> keys = cat.getKeys();
-        for (Key<?> key : keys) {
-            if (key.isPartialCategoryKey()) {
-                String name = key.getName();
-                query.put(name, toInsert.get(name));
-            }
-        }
         coll.update(query, toInsert, true, false);
     }
 
--- a/storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoStorageTest.java	Sun Aug 25 18:32:31 2013 -0600
+++ b/storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoStorageTest.java	Thu Aug 22 15:05:29 2013 +0200
@@ -69,6 +69,7 @@
 import org.powermock.core.classloader.annotations.PrepareForTest;
 import org.powermock.modules.junit4.PowerMockRunner;
 
+import com.mongodb.BasicDBList;
 import com.mongodb.BasicDBObject;
 import com.mongodb.DB;
 import com.mongodb.DBCollection;
@@ -452,7 +453,7 @@
     }
 
     @Test
-    public void verifyInsertReplaceCallsUpdate() {
+    public void verifyReplace() {
         TestClass pojo = new TestClass();
         pojo.setAgentId("123");
         pojo.setKey1("test1");
@@ -463,6 +464,11 @@
 
         MongoStorage storage = makeStorage();
         Replace replace = storage.createReplace(testCategory);
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression first = factory.equalTo(key1, "test1");
+        Expression second = factory.equalTo(key2, "test2");
+        Expression and = factory.and(first, second);
+        replace.where(and);
         replace.setPojo(pojo);
         replace.apply();
 
@@ -471,9 +477,16 @@
         verify(testCollection).update(queryCaptor.capture(), valueCaptor.capture(), eq(true), eq(false));
 
         DBObject query = queryCaptor.getValue();
-        assertEquals(2, query.keySet().size());
-        assertEquals("test1", query.get("key1"));
-        assertEquals("test2", query.get("key2"));
+        assertEquals("expected explicit and query", 1, query.keySet().size());
+        Object andObj = query.get("$and");
+        assertNotNull(andObj);
+        assertTrue(andObj instanceof BasicDBList);
+        BasicDBList list = (BasicDBList)andObj;
+        assertEquals("expected two operands", 2, list.size());
+        DBObject firstCond = (DBObject)list.get(0);
+        DBObject secondCond = (DBObject)list.get(1);
+        assertEquals("test1", firstCond.get("key1"));
+        assertEquals("test2", secondCond.get("key2"));
 
         DBObject value = valueCaptor.getValue();
         assertEquals(6, value.keySet().size());
--- a/thread/collector/src/main/java/com/redhat/thermostat/thread/dao/impl/ThreadDaoImpl.java	Sun Aug 25 18:32:31 2013 -0600
+++ b/thread/collector/src/main/java/com/redhat/thermostat/thread/dao/impl/ThreadDaoImpl.java	Thu Aug 22 15:05:29 2013 +0200
@@ -55,6 +55,8 @@
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.core.VmRef;
 import com.redhat.thermostat.storage.model.Pojo;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 import com.redhat.thermostat.thread.dao.ThreadDao;
 import com.redhat.thermostat.thread.model.ThreadHarvestingStatus;
 import com.redhat.thermostat.thread.model.ThreadInfoData;
@@ -137,7 +139,16 @@
     @Override
     public void saveCapabilities(VMThreadCapabilities caps) {
         Replace replace = storage.createReplace(THREAD_CAPABILITIES);
+        ExpressionFactory factory = new ExpressionFactory();
+        String agentId = caps.getAgentId();
+        if (agentId == null) {
+            agentId = storage.getAgentId();
+        }
+        Expression agentKey = factory.equalTo(Key.AGENT_ID, agentId);
+        Expression vmKey = factory.equalTo(Key.VM_ID, caps.getVmId());
+        Expression and = factory.and(agentKey, vmKey);
         replace.setPojo(caps);
+        replace.where(and);
         replace.apply();
     }
     
--- a/thread/collector/src/test/java/com/redhat/thermostat/thread/dao/impl/ThreadDaoImplTest.java	Sun Aug 25 18:32:31 2013 -0600
+++ b/thread/collector/src/test/java/com/redhat/thermostat/thread/dao/impl/ThreadDaoImplTest.java	Thu Aug 22 15:05:29 2013 +0200
@@ -40,6 +40,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertNull;
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -55,6 +56,7 @@
 import com.redhat.thermostat.storage.core.Cursor;
 import com.redhat.thermostat.storage.core.DescriptorParsingException;
 import com.redhat.thermostat.storage.core.HostRef;
+import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.PreparedStatement;
 import com.redhat.thermostat.storage.core.Replace;
 import com.redhat.thermostat.storage.core.StatementDescriptor;
@@ -62,6 +64,8 @@
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.core.VmRef;
 import com.redhat.thermostat.storage.model.Pojo;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 import com.redhat.thermostat.thread.dao.ThreadDao;
 import com.redhat.thermostat.thread.model.ThreadHarvestingStatus;
 import com.redhat.thermostat.thread.model.VMThreadCapabilities;
@@ -173,22 +177,64 @@
         assertEquals(null, caps);
     }
 
+    /*
+     * Tests saving of VMCapabilities when agentId has been explicitly set
+     * in thread capabilities model class.
+     */
     @Test
     public void testSaveVMCapabilities() {
+        String agentId = "fooAgent";
+        doTestSaveVMCaps(false, agentId);
+    }
+    
+    /*
+     * Tests saving of VMCapabilities when agentId has NOT been explicitly set
+     * in thread capabilities model class. AgentId should get filled in from
+     * storage.
+     */
+    @Test
+    public void testSaveVMCapabilitiesWithNoAgentIdExplicitlySet() {
+        String agentId = "fooStorageAgent";
+        doTestSaveVMCaps(true, agentId);
+    }
+    
+    private void doTestSaveVMCaps(boolean agentIdFromStorage, String agentId) {
         Storage storage = mock(Storage.class);
         Replace replace = mock(Replace.class);
         when(storage.createReplace(any(Category.class))).thenReturn(replace);
-
-        VMThreadCapabilities caps = mock(VMThreadCapabilities.class);
-        when(caps.supportContentionMonitor()).thenReturn(true);
-        when(caps.supportCPUTime()).thenReturn(true);
-        when(caps.supportThreadAllocatedMemory()).thenReturn(true);
-        when(caps.getVmId()).thenReturn("VM42");
+        if (agentIdFromStorage) {
+            when(storage.getAgentId()).thenReturn(agentId);
+        }
+        
+        String vmId = "VM42";
+        VMThreadCapabilities caps = new VMThreadCapabilities();
+        String[] capsFeatures = new String[] {
+                ThreadDao.CONTENTION_MONITOR,
+                ThreadDao.CPU_TIME,
+                ThreadDao.THREAD_ALLOCATED_MEMORY,
+        };
+        caps.setSupportedFeaturesList(capsFeatures);
+        assertTrue(caps.supportContentionMonitor());
+        assertTrue(caps.supportCPUTime());
+        assertTrue(caps.supportThreadAllocatedMemory());
+        caps.setVmId(vmId);
+        if (!agentIdFromStorage) {
+            caps.setAgentId(agentId);
+        } else {
+            // case where we want to have agentId null on caps itself.
+            assertNull(caps.getAgentId());
+        }
         ThreadDaoImpl dao = new ThreadDaoImpl(storage);
         dao.saveCapabilities(caps);
-
+        
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression agentExpr = factory.equalTo(Key.AGENT_ID, agentId);
+        Expression vmExpr = factory.equalTo(Key.VM_ID, vmId);
+        Expression expected = factory.and(agentExpr, vmExpr);
+        
         verify(storage).createReplace(ThreadDao.THREAD_CAPABILITIES);
         verify(replace).setPojo(caps);
+        verify(replace).where(expected);
         verify(replace).apply();
     }
 
--- a/web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorage.java	Sun Aug 25 18:32:31 2013 -0600
+++ b/web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorage.java	Thu Aug 22 15:05:29 2013 +0200
@@ -91,7 +91,6 @@
 import com.redhat.thermostat.storage.config.StartupConfiguration;
 import com.redhat.thermostat.storage.core.Add;
 import com.redhat.thermostat.storage.core.AuthToken;
-import com.redhat.thermostat.storage.core.BasePut;
 import com.redhat.thermostat.storage.core.Category;
 import com.redhat.thermostat.storage.core.Connection;
 import com.redhat.thermostat.storage.core.Cursor;
@@ -116,13 +115,14 @@
 import com.redhat.thermostat.web.common.OperatorSerializer;
 import com.redhat.thermostat.web.common.PreparedParameterSerializer;
 import com.redhat.thermostat.web.common.ThermostatGSONConverter;
-import com.redhat.thermostat.web.common.WebInsert;
+import com.redhat.thermostat.web.common.WebAdd;
 import com.redhat.thermostat.web.common.WebPreparedStatement;
 import com.redhat.thermostat.web.common.WebPreparedStatementResponse;
 import com.redhat.thermostat.web.common.WebPreparedStatementSerializer;
 import com.redhat.thermostat.web.common.WebQueryResponse;
 import com.redhat.thermostat.web.common.WebQueryResponseSerializer;
 import com.redhat.thermostat.web.common.WebRemove;
+import com.redhat.thermostat.web.common.WebReplace;
 import com.redhat.thermostat.web.common.WebUpdate;
 
 public class WebStorage implements Storage, SecureStorage {
@@ -299,30 +299,28 @@
 
     }
 
-    private class WebAdd extends BasePut implements Add {
+    private class WebAddImpl extends WebAdd {
 
-        private WebAdd(Category<?> category) {
-            super(category);
+        private WebAddImpl(int categoryId) {
+            super(categoryId);
         }
         
         @Override
         public void apply() {
-            int categoryId = getCategoryId(getCategory());
-            addImpl(new WebInsert(categoryId), getPojo());
+            addImpl(this);
         }
         
     }
 
-    private class WebReplace extends BasePut implements Replace {
-
-        private WebReplace(Category<?> category) {
-            super(category);
+    private class WebReplaceImpl extends WebReplace {
+        
+        private WebReplaceImpl(int categoryId) {
+            super(categoryId);
         }
         
         @Override
         public void apply() {
-            int categoryId = getCategoryId(getCategory());
-            replaceImpl(new WebInsert(categoryId), getPojo());
+            replaceImpl(this);
         }
         
     }
@@ -638,34 +636,38 @@
 
     @Override
     public Add createAdd(Category<?> into) {
-        WebAdd add = new WebAdd(into);
+        int categoryId = getCategoryId(into);
+        WebAdd add = new WebAddImpl(categoryId);
         return add;
     }
 
     @Override
     public Replace createReplace(Category<?> into) {
-        WebReplace replace = new WebReplace(into);
+        int categoryId = getCategoryId(into);
+        WebReplace replace = new WebReplaceImpl(categoryId);
         return replace;
     }
     
-    private void addImpl(WebInsert insert, final Pojo pojo) throws StorageException {
-        List<NameValuePair> formParams = getPutFormParams(insert, pojo);
+    private void addImpl(final WebAdd add) throws StorageException {
+        Pojo pojo = add.getPojo();
+        maybeAddAgentId(pojo);
+        NameValuePair pojoParam = new BasicNameValuePair("pojo",
+                gson.toJson(pojo));
+        NameValuePair addParam = new BasicNameValuePair("add",
+                gson.toJson(add));
+        List<NameValuePair> formParams = Arrays.asList(addParam, pojoParam);
         post(endpoint + "/add-pojo", formParams).close();
     }
 
-    private List<NameValuePair> getPutFormParams(WebInsert insert, Pojo pojo) {
+    private void replaceImpl(final WebReplace replace) throws StorageException {
+        Pojo pojo = replace.getPojo();
         maybeAddAgentId(pojo);
-        NameValuePair insertParam = new BasicNameValuePair("insert",
-                gson.toJson(insert));
+        NameValuePair replaceParam = new BasicNameValuePair("replace",
+                gson.toJson(replace));
         NameValuePair pojoParam = new BasicNameValuePair("pojo",
                 gson.toJson(pojo));
-        List<NameValuePair> formparams = Arrays.asList(insertParam, pojoParam);
-        return formparams;
-    }
-
-    private void replaceImpl(WebInsert insert, final Pojo pojo) throws StorageException {
-        List<NameValuePair> formparams = getPutFormParams(insert, pojo);
-        post(endpoint + "/replace-pojo", formparams).close();
+        List<NameValuePair> formParams = Arrays.asList(replaceParam, pojoParam);
+        post(endpoint + "/replace-pojo", formParams).close();
     }
 
     private void maybeAddAgentId(final Pojo pojo) throws AssertionError {
--- a/web/client/src/test/java/com/redhat/thermostat/web/client/internal/WebStorageTest.java	Sun Aug 25 18:32:31 2013 -0600
+++ b/web/client/src/test/java/com/redhat/thermostat/web/client/internal/WebStorageTest.java	Thu Aug 22 15:05:29 2013 +0200
@@ -90,6 +90,7 @@
 import com.google.gson.JsonParser;
 import com.google.gson.JsonSyntaxException;
 import com.redhat.thermostat.storage.config.StartupConfiguration;
+import com.redhat.thermostat.storage.core.Add;
 import com.redhat.thermostat.storage.core.AuthToken;
 import com.redhat.thermostat.storage.core.Categories;
 import com.redhat.thermostat.storage.core.Category;
@@ -117,13 +118,14 @@
 import com.redhat.thermostat.web.common.OperatorSerializer;
 import com.redhat.thermostat.web.common.PreparedParameterSerializer;
 import com.redhat.thermostat.web.common.ThermostatGSONConverter;
-import com.redhat.thermostat.web.common.WebInsert;
+import com.redhat.thermostat.web.common.WebAdd;
 import com.redhat.thermostat.web.common.WebPreparedStatement;
 import com.redhat.thermostat.web.common.WebPreparedStatementResponse;
 import com.redhat.thermostat.web.common.WebPreparedStatementSerializer;
 import com.redhat.thermostat.web.common.WebQueryResponse;
 import com.redhat.thermostat.web.common.WebQueryResponseSerializer;
 import com.redhat.thermostat.web.common.WebRemove;
+import com.redhat.thermostat.web.common.WebReplace;
 import com.redhat.thermostat.web.common.WebUpdate;
 
 public class WebStorageTest {
@@ -389,6 +391,43 @@
             // Pass.
         }
     }
+    
+    @Test
+    public void testAdd() throws IOException, JsonSyntaxException, ClassNotFoundException {
+
+        TestObj obj = new TestObj();
+        obj.setProperty1("fluff");
+
+        // We need an agentId, so that we can check automatic insert of agentId.
+        UUID agentId = new UUID(1, 2);
+        storage.setAgentId(agentId);
+
+        Add add = storage.createAdd(category);
+        add.setPojo(obj);
+
+        prepareServer();
+        add.apply();
+
+        Gson gson = new Gson();
+        StringReader reader = new StringReader(requestBody);
+        BufferedReader bufRead = new BufferedReader(reader);
+        String line = URLDecoder.decode(bufRead.readLine(), "UTF-8");
+        String [] params = line.split("&");
+        assertEquals(2, params.length);
+        String[] parts = params[0].split("=");
+        assertEquals("add", parts[0]);
+        WebAdd add2 = gson.fromJson(parts[1], WebAdd.class);
+        assertEquals(42, add2.getCategoryId());
+
+        parts = params[1].split("=");
+        assertEquals(2, parts.length);
+        assertEquals("pojo", parts[0]);
+        Object resultObj = gson.fromJson(parts[1], TestObj.class);
+
+        // Set agentId on expected object, because we expect WebStorage to insert it for us.
+        obj.setAgentId(agentId.toString());
+        assertEquals(obj, resultObj);
+    }
 
     @Test
     public void testReplace() throws IOException, JsonSyntaxException, ClassNotFoundException {
@@ -401,20 +440,26 @@
         storage.setAgentId(agentId);
 
         Replace replace = storage.createReplace(category);
+        Expression expr = new ExpressionFactory().equalTo(key1, "fluff");
         replace.setPojo(obj);
+        replace.where(expr);
 
         prepareServer();
         replace.apply();
 
-        Gson gson = new Gson();
+        Gson gson = new GsonBuilder()
+                .registerTypeHierarchyAdapter(Expression.class,
+                        new ExpressionSerializer())
+                .registerTypeHierarchyAdapter(Operator.class,
+                        new OperatorSerializer()).create();
         StringReader reader = new StringReader(requestBody);
         BufferedReader bufRead = new BufferedReader(reader);
         String line = URLDecoder.decode(bufRead.readLine(), "UTF-8");
         String [] params = line.split("&");
         assertEquals(2, params.length);
         String[] parts = params[0].split("=");
-        assertEquals("insert", parts[0]);
-        WebInsert insert = gson.fromJson(parts[1], WebInsert.class);
+        assertEquals("replace", parts[0]);
+        WebReplace insert = gson.fromJson(parts[1], WebReplace.class);
         assertEquals(42, insert.getCategoryId());
 
         parts = params[1].split("=");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/common/src/main/java/com/redhat/thermostat/web/common/WebAdd.java	Thu Aug 22 15:05:29 2013 +0200
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+
+package com.redhat.thermostat.web.common;
+
+import com.redhat.thermostat.storage.core.Add;
+import com.redhat.thermostat.storage.model.Pojo;
+
+
+public class WebAdd implements Add {
+
+    private int categoryId;
+    private transient Pojo pojo;
+
+    public WebAdd() {
+    }
+
+    public WebAdd(int categoryId) {
+        this.categoryId = categoryId;
+    }
+
+    public int getCategoryId() {
+        return categoryId;
+    }
+
+    public void setCategoryId(int categoryId) {
+        this.categoryId = categoryId;
+    }
+
+
+    @Override
+    public void setPojo(Pojo pojo) {
+        this.pojo = pojo;
+    }
+    
+    public Pojo getPojo() {
+        return pojo;
+    }
+
+    @Override
+    public void apply() {
+        // Should never be called directly, but overridden by
+        // the actual implementation. Here only so that it can be used
+        // for serialization.
+        throw new IllegalStateException();
+    }
+}
+
--- a/web/common/src/main/java/com/redhat/thermostat/web/common/WebInsert.java	Sun Aug 25 18:32:31 2013 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-/*
- * Copyright 2012, 2013 Red Hat, Inc.
- *
- * This file is part of Thermostat.
- *
- * Thermostat is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published
- * by the Free Software Foundation; either version 2, or (at your
- * option) any later version.
- *
- * Thermostat is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Thermostat; see the file COPYING.  If not see
- * <http://www.gnu.org/licenses/>.
- *
- * Linking this code with other modules is making a combined work
- * based on this code.  Thus, the terms and conditions of the GNU
- * General Public License cover the whole combination.
- *
- * As a special exception, the copyright holders of this code give
- * you permission to link this code with independent modules to
- * produce an executable, regardless of the license terms of these
- * independent modules, and to copy and distribute the resulting
- * executable under terms of your choice, provided that you also
- * meet, for each linked independent module, the terms and conditions
- * of the license of that module.  An independent module is a module
- * which is not derived from or based on this code.  If you modify
- * this code, you may extend this exception to your version of the
- * library, but you are not obligated to do so.  If you do not wish
- * to do so, delete this exception statement from your version.
- */
-
-
-package com.redhat.thermostat.web.common;
-
-
-public class WebInsert {
-
-    private int categoryId;
-
-    public WebInsert() {
-    }
-
-    public WebInsert(int categoryId) {
-        this.categoryId = categoryId;
-    }
-
-    public int getCategoryId() {
-        return categoryId;
-    }
-
-    public void setCategoryId(int categoryId) {
-        this.categoryId = categoryId;
-    }
-
-}
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/common/src/main/java/com/redhat/thermostat/web/common/WebReplace.java	Thu Aug 22 15:05:29 2013 +0200
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.web.common;
+
+import com.redhat.thermostat.storage.core.Replace;
+import com.redhat.thermostat.storage.model.Pojo;
+import com.redhat.thermostat.storage.query.Expression;
+
+public class WebReplace implements Replace {
+    
+    private int categoryId;
+    private transient Pojo pojo;
+    private Expression whereExpression;
+    
+    public WebReplace() {
+        // nothing
+    }
+    
+    public WebReplace(int categoryId) {
+        this.categoryId = categoryId;
+    }
+
+    @Override
+    public void setPojo(Pojo pojo) {
+        this.pojo = pojo;
+    }
+
+    @Override
+    public void apply() {
+        // Should never be called directly, but overridden by
+        // the actual implementation. Here only so that it can be used
+        // for serialization.
+        throw new IllegalStateException();
+    }
+
+    @Override
+    public void where(Expression expression) {
+        this.whereExpression = expression;
+    }
+
+    public int getCategoryId() {
+        return categoryId;
+    }
+
+    public void setCategoryId(int categoryId) {
+        this.categoryId = categoryId;
+    }
+
+    public Pojo getPojo() {
+        return pojo;
+    }
+
+    public Expression getWhereExpression() {
+        return whereExpression;
+    }
+
+}
--- a/web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java	Sun Aug 25 18:32:31 2013 -0600
+++ b/web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java	Thu Aug 22 15:05:29 2013 +0200
@@ -106,13 +106,14 @@
 import com.redhat.thermostat.web.common.PreparedParameterSerializer;
 import com.redhat.thermostat.web.common.StorageWrapper;
 import com.redhat.thermostat.web.common.ThermostatGSONConverter;
-import com.redhat.thermostat.web.common.WebInsert;
+import com.redhat.thermostat.web.common.WebAdd;
 import com.redhat.thermostat.web.common.WebPreparedStatement;
 import com.redhat.thermostat.web.common.WebPreparedStatementResponse;
 import com.redhat.thermostat.web.common.WebPreparedStatementSerializer;
 import com.redhat.thermostat.web.common.WebQueryResponse;
 import com.redhat.thermostat.web.common.WebQueryResponseSerializer;
 import com.redhat.thermostat.web.common.WebRemove;
+import com.redhat.thermostat.web.common.WebReplace;
 import com.redhat.thermostat.web.common.WebUpdate;
 import com.redhat.thermostat.web.server.auth.FilterResult;
 import com.redhat.thermostat.web.server.auth.Roles;
@@ -526,9 +527,9 @@
         if (! isAuthorized(req, resp, Roles.APPEND)) {
             return;
         }
-        String insertParam = req.getParameter("insert");
-        WebInsert insert = gson.fromJson(insertParam, WebInsert.class);
-        int categoryId = insert.getCategoryId();
+        String addParam = req.getParameter("add");
+        WebAdd add = gson.fromJson(addParam, WebAdd.class);
+        int categoryId = add.getCategoryId();
         Category<?> category = getCategoryFromId(categoryId);
         Add targetAdd = storage.createAdd(category);
         Class<? extends Pojo> pojoCls = category.getDataClass();
@@ -544,15 +545,17 @@
         if (! isAuthorized(req, resp, Roles.REPLACE)) {
             return;
         }
-        String insertParam = req.getParameter("insert");
-        WebInsert insert = gson.fromJson(insertParam, WebInsert.class);
-        int categoryId = insert.getCategoryId();
+        String replaceParam = req.getParameter("replace");
+        WebReplace replace = gson.fromJson(replaceParam, WebReplace.class);
+        int categoryId = replace.getCategoryId();
         Category<?> category = getCategoryFromId(categoryId);
         Replace targetReplace = storage.createReplace(category);
         Class<? extends Pojo> pojoCls = category.getDataClass();
         String pojoParam = req.getParameter("pojo");
         Pojo pojo = gson.fromJson(pojoParam, pojoCls);
         targetReplace.setPojo(pojo);
+        Expression expr = replace.getWhereExpression();
+        targetReplace.where(expr);
         targetReplace.apply();
         resp.setStatus(HttpServletResponse.SC_OK);
     }
--- a/web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java	Sun Aug 25 18:32:31 2013 -0600
+++ b/web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java	Thu Aug 22 15:05:29 2013 +0200
@@ -113,8 +113,8 @@
 import com.redhat.thermostat.storage.core.Replace;
 import com.redhat.thermostat.storage.core.StatementDescriptor;
 import com.redhat.thermostat.storage.core.Update;
+import com.redhat.thermostat.storage.core.auth.CategoryRegistration;
 import com.redhat.thermostat.storage.core.auth.DescriptorMetadata;
-import com.redhat.thermostat.storage.core.auth.CategoryRegistration;
 import com.redhat.thermostat.storage.core.auth.StatementDescriptorRegistration;
 import com.redhat.thermostat.storage.dao.HostInfoDAO;
 import com.redhat.thermostat.storage.model.AggregateCount;
@@ -132,13 +132,14 @@
 import com.redhat.thermostat.web.common.PreparedParameterSerializer;
 import com.redhat.thermostat.web.common.StorageWrapper;
 import com.redhat.thermostat.web.common.ThermostatGSONConverter;
-import com.redhat.thermostat.web.common.WebInsert;
+import com.redhat.thermostat.web.common.WebAdd;
 import com.redhat.thermostat.web.common.WebPreparedStatement;
 import com.redhat.thermostat.web.common.WebPreparedStatementResponse;
 import com.redhat.thermostat.web.common.WebPreparedStatementSerializer;
 import com.redhat.thermostat.web.common.WebQueryResponse;
 import com.redhat.thermostat.web.common.WebQueryResponseSerializer;
 import com.redhat.thermostat.web.common.WebRemove;
+import com.redhat.thermostat.web.common.WebReplace;
 import com.redhat.thermostat.web.common.WebUpdate;
 import com.redhat.thermostat.web.server.auth.BasicRole;
 import com.redhat.thermostat.web.server.auth.RolePrincipal;
@@ -900,6 +901,7 @@
         TestClass expected1 = new TestClass();
         expected1.setKey1("fluff1");
         expected1.setKey2(42);
+        Expression expectedExpression = new ExpressionFactory().equalTo(key1, "fluff1");
 
         String endpoint = getEndpoint();
 
@@ -910,11 +912,16 @@
 
         conn.setDoOutput(true);
         conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
-        WebInsert insert = new WebInsert(categoryId);
-        Gson gson = new Gson();
+        WebReplace webReplace = new WebReplace(categoryId);
+        webReplace.where(expectedExpression);
+        Gson gson = new GsonBuilder()
+            .registerTypeHierarchyAdapter(Expression.class,
+                    new ExpressionSerializer())
+            .registerTypeHierarchyAdapter(Operator.class,
+                    new OperatorSerializer()).create();
         OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
-        out.write("insert=");
-        gson.toJson(insert, out);
+        out.write("replace=");
+        gson.toJson(webReplace, out);
         out.flush();
         out.write("&pojo=");
         gson.toJson(expected1, out);
@@ -923,6 +930,7 @@
         assertEquals(200, conn.getResponseCode());
         verify(mockStorage).createReplace(category);
         verify(replace).setPojo(expected1);
+        verify(replace).where(eq(expectedExpression));
         verify(replace).apply();
     }    
     
@@ -955,12 +963,15 @@
         conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
         conn.setDoOutput(true);
         conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
-        // replace
-        WebInsert insert = new WebInsert(categoryId);
-        Gson gson = new Gson();
+        WebReplace webReplace = new WebReplace(categoryId);
+        Gson gson = new GsonBuilder()
+            .registerTypeHierarchyAdapter(Expression.class,
+                new ExpressionSerializer())
+            .registerTypeHierarchyAdapter(Operator.class,
+                new OperatorSerializer()).create();
         OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
-        out.write("insert=");
-        gson.toJson(insert, out);
+        out.write("replace=");
+        gson.toJson(webReplace, out);
         out.flush();
         out.write("&pojo=");
         TestClass expected1 = new TestClass();
@@ -1009,10 +1020,10 @@
 
         conn.setDoOutput(true);
         conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
-        WebInsert ins = new WebInsert(categoryId);
+        WebAdd ins = new WebAdd(categoryId);
         Gson gson = new Gson();
         OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
-        out.write("insert=");
+        out.write("add=");
         gson.toJson(ins, out);
         out.flush();
         out.write("&pojo=");
@@ -1054,11 +1065,10 @@
         conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
         conn.setDoOutput(true);
         conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
-        // replace
-        WebInsert insert = new WebInsert(categoryId);
+        WebAdd insert = new WebAdd(categoryId);
         Gson gson = new Gson();
         OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
-        out.write("insert=");
+        out.write("add=");
         gson.toJson(insert, out);
         out.flush();
         out.write("&pojo=");