changeset 1140:bf720980510c

Handle boolean formulas in Query.where This commit adds a hierarchy of expressions to storage-core. These expressions are used to create more general boolean formulas for queries than we currently support. Most importantly, this will allow us to use disjunctions in queries. Expressions are created using the ExpressionFactory methods corresponding to each operator. For instance, expressions created by the factory's "greaterThan" and "lessThan" methods can then be joined using the factory's "and" or "or" methods. These expressions are serialized/deserialized to/from JSON by the new ExpressionSerializer and OperatorSerializer classes. These serializers are written to only handle Expression subclasses that it knows about, and these concrete Expression classes are all declared final. This should help prevent the web service from handling malicious arbitrary queries. Ideally I would have liked to make all Expression constructors package-private and require that all instantiations be done through the factory, but the need to deserialize expressions from JSON prevents this unless we want storage-core to deal with JSON. The MongoDB storage backend uses a new MongoExpressionParser class to convert an expression into a Mongo query. Conjunctions are handled differently now. MongoDB supports implicit and explicit conjunctions. Previously, our MongoQuery.where appended new clauses to the query in each successive call. This resulted in an implicit conjunction of these clauses. Now we create explicit conjunctions using the $and operator. This has a couple of advantages: short-circuiting, and the ability to specify the same key twice (e.g. x > 7 && x < 10). Reviewed-by: jerboaa Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-June/006932.html
author Elliott Baron <ebaron@redhat.com>
date Fri, 07 Jun 2013 13:49:04 -0400
parents 85cd7afdf70f
children 2e71b9784316
files host-cpu/common/src/test/java/com/redhat/thermostat/host/cpu/common/internal/CpuStatDAOTest.java host-memory/common/src/test/java/com/redhat/thermostat/host/memory/common/internal/MemoryStatDAOTest.java numa/common/src/main/java/com/redhat/thermostat/numa/common/internal/NumaDAOImpl.java storage/core/pom.xml storage/core/src/main/java/com/redhat/thermostat/storage/core/HostLatestPojoListGetter.java storage/core/src/main/java/com/redhat/thermostat/storage/core/Query.java storage/core/src/main/java/com/redhat/thermostat/storage/core/VmLatestPojoListGetter.java storage/core/src/main/java/com/redhat/thermostat/storage/internal/dao/AgentInfoDAOImpl.java storage/core/src/main/java/com/redhat/thermostat/storage/internal/dao/BackendInfoDAOImpl.java storage/core/src/main/java/com/redhat/thermostat/storage/internal/dao/HostInfoDAOImpl.java storage/core/src/main/java/com/redhat/thermostat/storage/internal/dao/NetworkInterfaceInfoDAOImpl.java storage/core/src/main/java/com/redhat/thermostat/storage/internal/dao/VmInfoDAOImpl.java storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryComparisonExpression.java storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryComparisonOperator.java storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryExpression.java storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryLogicalExpression.java storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryLogicalOperator.java storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryOperator.java storage/core/src/main/java/com/redhat/thermostat/storage/query/Expression.java storage/core/src/main/java/com/redhat/thermostat/storage/query/ExpressionFactory.java storage/core/src/main/java/com/redhat/thermostat/storage/query/LiteralExpression.java storage/core/src/main/java/com/redhat/thermostat/storage/query/Operator.java storage/core/src/main/java/com/redhat/thermostat/storage/query/UnaryExpression.java storage/core/src/main/java/com/redhat/thermostat/storage/query/UnaryLogicalExpression.java storage/core/src/main/java/com/redhat/thermostat/storage/query/UnaryLogicalOperator.java storage/core/src/main/java/com/redhat/thermostat/storage/query/UnaryOperator.java storage/core/src/test/java/com/redhat/thermostat/storage/core/HostLatestPojoListGetterTest.java storage/core/src/test/java/com/redhat/thermostat/storage/core/VmLatestPojoListGetterTest.java storage/core/src/test/java/com/redhat/thermostat/storage/internal/dao/AgentInfoDAOTest.java storage/core/src/test/java/com/redhat/thermostat/storage/internal/dao/BackendInfoDAOTest.java storage/core/src/test/java/com/redhat/thermostat/storage/internal/dao/NetworkInterfaceInfoDAOTest.java storage/core/src/test/java/com/redhat/thermostat/storage/query/BinaryExpressionTest.java storage/core/src/test/java/com/redhat/thermostat/storage/query/ExpressionFactoryTest.java storage/core/src/test/java/com/redhat/thermostat/storage/query/LiteralExpressionTest.java storage/core/src/test/java/com/redhat/thermostat/storage/query/UnaryExpressionTest.java storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/MongoExpressionParser.java storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/MongoQuery.java storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoExpressionParserTest.java storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoQueryTest.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 vm-classstat/common/src/test/java/com/redhat/thermostat/vm/classstat/common/internal/VmClassStatDAOTest.java vm-cpu/common/src/test/java/com/redhat/thermostat/vm/cpu/common/internal/VmCpuStatDAOTest.java vm-gc/common/src/test/java/com/redhat/thermostat/vm/gc/common/internal/VmGcStatDAOTest.java vm-heap-analysis/common/src/main/java/com/redhat/thermostat/vm/heap/analysis/common/internal/HeapDAOImpl.java vm-jmx/common/src/main/java/com/redhat/thermostat/vm/jmx/common/internal/JmxNotificationDAOImpl.java vm-jmx/common/src/test/java/com/redhat/thermostat/vm/jmx/common/internal/JmxNotificationDAOImplTest.java vm-memory/common/src/main/java/com/redhat/thermostat/vm/memory/common/internal/VmMemoryStatDAOImpl.java vm-memory/common/src/test/java/com/redhat/thermostat/vm/memory/common/internal/VmMemoryStatDAOTest.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/ExpressionSerializer.java web/common/src/main/java/com/redhat/thermostat/web/common/OperatorSerializer.java web/common/src/main/java/com/redhat/thermostat/web/common/WebQuery.java web/common/src/main/java/com/redhat/thermostat/web/common/WebRemove.java web/common/src/test/java/com/redhat/thermostat/web/common/ExpressionSerializerTest.java web/common/src/test/java/com/redhat/thermostat/web/common/OperatorSerializerTest.java web/common/src/test/java/com/redhat/thermostat/web/common/WebQueryTest.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 61 files changed, 2851 insertions(+), 339 deletions(-) [+]
line wrap: on
line diff
--- a/host-cpu/common/src/test/java/com/redhat/thermostat/host/cpu/common/internal/CpuStatDAOTest.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/host-cpu/common/src/test/java/com/redhat/thermostat/host/cpu/common/internal/CpuStatDAOTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -40,7 +40,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.same;
+import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
@@ -53,16 +53,17 @@
 
 import org.junit.Test;
 
-import com.redhat.thermostat.storage.core.HostRef;
 import com.redhat.thermostat.host.cpu.common.CpuStatDAO;
 import com.redhat.thermostat.host.cpu.common.model.CpuStat;
 import com.redhat.thermostat.storage.core.Add;
 import com.redhat.thermostat.storage.core.Category;
 import com.redhat.thermostat.storage.core.Cursor;
+import com.redhat.thermostat.storage.core.HostRef;
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Storage;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 
 public class CpuStatDAOTest {
 
@@ -99,8 +100,8 @@
 
         List<CpuStat> cpuStats = dao.getLatestCpuStats(hostRef, Long.MIN_VALUE);
 
-        verify(query).where(Key.TIMESTAMP, Criteria.GREATER_THAN, Long.MIN_VALUE);
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, "system");
+        Expression expr = createWhereExpression();
+        verify(query).where(eq(expr));
         verify(query).sort(Key.TIMESTAMP, Query.SortDirection.DESCENDING);
         verify(query).execute();
         verifyNoMoreInteractions(query);
@@ -112,6 +113,12 @@
 
     }
 
+    private Expression createWhereExpression() {
+        ExpressionFactory factory = new ExpressionFactory();
+        return factory.and(factory.equalTo(Key.AGENT_ID, "system"),
+                factory.greaterThan(Key.TIMESTAMP, Long.MIN_VALUE));
+    }
+
     @Test
     public void testGetLatestCpuStatsTwice() {
 
@@ -135,9 +142,9 @@
         dao.getLatestCpuStats(hostRef, Long.MIN_VALUE);
         dao.getLatestCpuStats(hostRef, Long.MIN_VALUE);
 
+        Expression expr = createWhereExpression();
         verify(query, times(2)).execute();
-        verify(query, atLeastOnce()).where(same(Key.AGENT_ID), same(Criteria.EQUALS), any());
-        verify(query, atLeastOnce()).where(same(Key.TIMESTAMP), any(Criteria.class), any());
+        verify(query, atLeastOnce()).where(eq(expr));
     }
 
     @Test
--- a/host-memory/common/src/test/java/com/redhat/thermostat/host/memory/common/internal/MemoryStatDAOTest.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/host-memory/common/src/test/java/com/redhat/thermostat/host/memory/common/internal/MemoryStatDAOTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -39,6 +39,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 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.verifyNoMoreInteractions;
@@ -49,16 +50,17 @@
 
 import org.junit.Test;
 
-import com.redhat.thermostat.storage.core.HostRef;
 import com.redhat.thermostat.host.memory.common.MemoryStatDAO;
 import com.redhat.thermostat.host.memory.common.model.MemoryStat;
 import com.redhat.thermostat.storage.core.Add;
 import com.redhat.thermostat.storage.core.Category;
 import com.redhat.thermostat.storage.core.Cursor;
+import com.redhat.thermostat.storage.core.HostRef;
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Storage;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 
 public class MemoryStatDAOTest {
 
@@ -109,8 +111,9 @@
         List<MemoryStat> memoryStats = dao.getLatestMemoryStats(hostRef, Long.MIN_VALUE);
 
         verify(storage).createQuery(MemoryStatDAO.memoryStatCategory);
-        verify(query).where(Key.TIMESTAMP, Criteria.GREATER_THAN, Long.MIN_VALUE);
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, "system");
+        
+        Expression expr = createWhereExpression();
+        verify(query).where(eq(expr));
         verify(query).sort(Key.TIMESTAMP, Query.SortDirection.DESCENDING);
         verify(query).execute();
         verifyNoMoreInteractions(query);
@@ -151,5 +154,11 @@
         Long count = dao.getCount();
         assertEquals((Long) 5L, count);
     }
+    
+    private Expression createWhereExpression() {
+        ExpressionFactory factory = new ExpressionFactory();
+        return factory.and(factory.equalTo(Key.AGENT_ID, "system"),
+                factory.greaterThan(Key.TIMESTAMP, Long.MIN_VALUE));
+    }
 }
 
--- a/numa/common/src/main/java/com/redhat/thermostat/numa/common/internal/NumaDAOImpl.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/numa/common/src/main/java/com/redhat/thermostat/numa/common/internal/NumaDAOImpl.java	Fri Jun 07 13:49:04 2013 -0400
@@ -47,9 +47,10 @@
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Put;
 import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Replace;
 import com.redhat.thermostat.storage.core.Storage;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 
 public class NumaDAOImpl implements NumaDAO {
 
@@ -88,7 +89,9 @@
     @Override
     public int getNumberOfNumaNodes(HostRef ref) {
         Query<NumaHostInfo> query = storage.createQuery(numaHostCategory);
-        query.where(Key.AGENT_ID, Criteria.EQUALS, ref.getAgentId());
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.equalTo(Key.AGENT_ID, ref.getAgentId());
+        query.where(expr);
         query.limit(1);
         Cursor<NumaHostInfo> numaHostInfo = query.execute();
         if (numaHostInfo.hasNext()) {
--- a/storage/core/pom.xml	Fri Jun 07 11:38:33 2013 -0400
+++ b/storage/core/pom.xml	Fri Jun 07 13:49:04 2013 -0400
@@ -67,6 +67,7 @@
               com.redhat.thermostat.storage.config,
               com.redhat.thermostat.storage.model,
               com.redhat.thermostat.storage.dao,
+              com.redhat.thermostat.storage.query,
             </Export-Package>
             <!-- TODO: For the thread tab (i.e. thread plug-in) the web server
                  bundle requires model classes provided by said bundle. Since
--- a/storage/core/src/main/java/com/redhat/thermostat/storage/core/HostLatestPojoListGetter.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/core/HostLatestPojoListGetter.java	Fri Jun 07 13:49:04 2013 -0400
@@ -39,13 +39,9 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import com.redhat.thermostat.storage.core.Category;
-import com.redhat.thermostat.storage.core.Cursor;
-import com.redhat.thermostat.storage.core.Key;
-import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
-import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.model.TimeStampedPojo;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 
 public class HostLatestPojoListGetter<T extends TimeStampedPojo> {
 
@@ -74,8 +70,11 @@
 
     protected Query<T> buildQuery(HostRef hostRef, long since) {
         Query<T> query = storage.createQuery(cat);
-        query.where(Key.AGENT_ID, Criteria.EQUALS, hostRef.getAgentId());
-        query.where(Key.TIMESTAMP, Criteria.GREATER_THAN, since);
+        // AGENT_ID == hostRef.getAgentId() && TIMESTAMP > since
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(factory.equalTo(Key.AGENT_ID, hostRef.getAgentId()), 
+                factory.greaterThan(Key.TIMESTAMP, since));
+        query.where(expr);
         query.sort(Key.TIMESTAMP, Query.SortDirection.DESCENDING);
         return query;
     }
--- a/storage/core/src/main/java/com/redhat/thermostat/storage/core/Query.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/core/Query.java	Fri Jun 07 13:49:04 2013 -0400
@@ -37,6 +37,7 @@
 package com.redhat.thermostat.storage.core;
 
 import com.redhat.thermostat.storage.model.Pojo;
+import com.redhat.thermostat.storage.query.Expression;
 
 /**
  * Describes what data should be fetched.
@@ -67,7 +68,7 @@
         }
     }
 
-    <S> void where(Key<S> key, Criteria criteria, S value);
+    void where(Expression expr);
     
     void sort(Key<?> key, SortDirection direction);
 
--- a/storage/core/src/main/java/com/redhat/thermostat/storage/core/VmLatestPojoListGetter.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/core/VmLatestPojoListGetter.java	Fri Jun 07 13:49:04 2013 -0400
@@ -39,13 +39,9 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import com.redhat.thermostat.storage.core.Category;
-import com.redhat.thermostat.storage.core.Cursor;
-import com.redhat.thermostat.storage.core.Key;
-import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
-import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.model.TimeStampedPojo;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 
 public class VmLatestPojoListGetter<T extends TimeStampedPojo> {
 
@@ -74,9 +70,12 @@
 
     protected Query<T> buildQuery(VmRef vmRef, long since) {
         Query<T> query = storage.createQuery(cat);
-        query.where(Key.AGENT_ID, Criteria.EQUALS, vmRef.getAgent().getAgentId());
-        query.where(Key.VM_ID, Criteria.EQUALS, vmRef.getId());
-        query.where(Key.TIMESTAMP, Criteria.GREATER_THAN, since);
+        // AGENT_ID == getAgentId() && VM_ID == getId() && TIMESTAMP > since
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(factory.equalTo(Key.AGENT_ID, vmRef.getAgent().getAgentId()),
+                factory.and(factory.equalTo(Key.VM_ID, vmRef.getId()),
+                        factory.greaterThan(Key.TIMESTAMP, since)));
+        query.where(expr);
         query.sort(Key.TIMESTAMP, Query.SortDirection.DESCENDING);
         return query;
     }
--- a/storage/core/src/main/java/com/redhat/thermostat/storage/internal/dao/AgentInfoDAOImpl.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/internal/dao/AgentInfoDAOImpl.java	Fri Jun 07 13:49:04 2013 -0400
@@ -47,9 +47,10 @@
 import com.redhat.thermostat.storage.core.Remove;
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.core.Update;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.dao.AgentInfoDAO;
 import com.redhat.thermostat.storage.model.AgentInformation;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 
 public class AgentInfoDAOImpl implements AgentInfoDAO {
 
@@ -82,7 +83,9 @@
     @Override
     public List<AgentInformation> getAliveAgents() {
         Query<AgentInformation> query = storage.createQuery(CATEGORY);
-        query.where(AgentInfoDAO.ALIVE_KEY, Criteria.EQUALS, true);
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.equalTo(ALIVE_KEY, Boolean.TRUE);
+        query.where(expr);
 
         Cursor<AgentInformation> agentCursor = query.execute();
 
@@ -98,7 +101,9 @@
     @Override
     public AgentInformation getAgentInformation(HostRef agentRef) {
         Query<AgentInformation> query = storage.createQuery(CATEGORY);
-        query.where(Key.AGENT_ID, Criteria.EQUALS, agentRef.getAgentId());
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.equalTo(Key.AGENT_ID, agentRef.getAgentId());
+        query.where(expr);
         query.limit(1);
         return query.execute().next();
     }
--- a/storage/core/src/main/java/com/redhat/thermostat/storage/internal/dao/BackendInfoDAOImpl.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/internal/dao/BackendInfoDAOImpl.java	Fri Jun 07 13:49:04 2013 -0400
@@ -46,11 +46,12 @@
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Put;
 import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Remove;
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.dao.BackendInfoDAO;
 import com.redhat.thermostat.storage.model.BackendInformation;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 
 public class BackendInfoDAOImpl implements BackendInfoDAO {
 
@@ -65,7 +66,9 @@
     public List<BackendInformation> getBackendInformation(HostRef host) {
         // Sort by order value
         Query<BackendInformation> query = storage.createQuery(CATEGORY);
-        query.where(Key.AGENT_ID, Criteria.EQUALS, host.getAgentId());
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.equalTo(Key.AGENT_ID, host.getAgentId());
+        query.where(expr);
 
         List<BackendInformation> results = new ArrayList<>();
         Cursor<BackendInformation> cursor = query.execute();
--- a/storage/core/src/main/java/com/redhat/thermostat/storage/internal/dao/HostInfoDAOImpl.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/internal/dao/HostInfoDAOImpl.java	Fri Jun 07 13:49:04 2013 -0400
@@ -46,11 +46,12 @@
 import com.redhat.thermostat.storage.core.Put;
 import com.redhat.thermostat.storage.core.Query;
 import com.redhat.thermostat.storage.core.Storage;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.dao.AgentInfoDAO;
 import com.redhat.thermostat.storage.dao.HostInfoDAO;
 import com.redhat.thermostat.storage.model.AgentInformation;
 import com.redhat.thermostat.storage.model.HostInfo;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 
 public class HostInfoDAOImpl implements HostInfoDAO {
 
@@ -67,7 +68,9 @@
     @Override
     public HostInfo getHostInfo(HostRef ref) {
         Query<HostInfo> query = storage.createQuery(hostInfoCategory);
-        query.where(Key.AGENT_ID, Criteria.EQUALS, ref.getAgentId());
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.equalTo(Key.AGENT_ID, ref.getAgentId());
+        query.where(expr);
         query.limit(1);
         HostInfo result = query.execute().next();
         return result;
@@ -92,7 +95,9 @@
         List<AgentInformation> agentInfos = agentInfoDao.getAliveAgents();
         for (AgentInformation agentInfo : agentInfos) {
             Query<HostInfo> filter = storage.createQuery(hostInfoCategory);
-            filter.where(Key.AGENT_ID, Criteria.EQUALS, agentInfo.getAgentId());
+            ExpressionFactory factory = new ExpressionFactory();
+            Expression expr = factory.equalTo(Key.AGENT_ID, agentInfo.getAgentId());
+            filter.where(expr);
             hosts.addAll(getHosts(filter));
         }
 
--- a/storage/core/src/main/java/com/redhat/thermostat/storage/internal/dao/NetworkInterfaceInfoDAOImpl.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/internal/dao/NetworkInterfaceInfoDAOImpl.java	Fri Jun 07 13:49:04 2013 -0400
@@ -44,10 +44,11 @@
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Put;
 import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 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 {
 
@@ -61,7 +62,9 @@
     @Override
     public List<NetworkInterfaceInfo> getNetworkInterfaces(HostRef ref) {
         Query<NetworkInterfaceInfo> allHostNetworkInterfaces = storage.createQuery(networkInfoCategory);
-        allHostNetworkInterfaces.where(Key.AGENT_ID, Criteria.EQUALS, ref.getAgentId());
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.equalTo(Key.AGENT_ID, ref.getAgentId());
+        allHostNetworkInterfaces.where(expr);
 
         Cursor<NetworkInterfaceInfo> cursor = allHostNetworkInterfaces.execute();
         List<NetworkInterfaceInfo> result = new ArrayList<>();
--- a/storage/core/src/main/java/com/redhat/thermostat/storage/internal/dao/VmInfoDAOImpl.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/internal/dao/VmInfoDAOImpl.java	Fri Jun 07 13:49:04 2013 -0400
@@ -45,13 +45,14 @@
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Put;
 import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.core.Update;
 import com.redhat.thermostat.storage.core.VmRef;
 import com.redhat.thermostat.storage.dao.DAOException;
 import com.redhat.thermostat.storage.dao.VmInfoDAO;
 import com.redhat.thermostat.storage.model.VmInfo;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 
 public class VmInfoDAOImpl implements VmInfoDAO {
 
@@ -65,8 +66,12 @@
     @Override
     public VmInfo getVmInfo(VmRef ref) {
         Query<VmInfo> findMatchingVm = storage.createQuery(vmInfoCategory);
-        findMatchingVm.where(Key.AGENT_ID, Criteria.EQUALS, ref.getAgent().getAgentId());
-        findMatchingVm.where(Key.VM_ID, Criteria.EQUALS, ref.getId());
+        // AGENT_ID == getAgentId() && VM_ID == getId()
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(
+                factory.equalTo(Key.AGENT_ID, ref.getAgent().getAgentId()),
+                factory.equalTo(Key.VM_ID, ref.getId()));
+        findMatchingVm.where(expr);
         findMatchingVm.limit(1);
         VmInfo result = findMatchingVm.execute().next();
         if (result == null) {
@@ -85,7 +90,9 @@
 
     private Query<VmInfo> buildQuery(HostRef host) {
         Query<VmInfo> query = storage.createQuery(vmInfoCategory);
-        query.where(Key.AGENT_ID, Criteria.EQUALS, host.getAgentId());
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.equalTo(Key.AGENT_ID, host.getAgentId());
+        query.where(expr);
         return query;
     }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryComparisonExpression.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,65 @@
+/*
+ * 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.storage.query;
+
+import com.redhat.thermostat.storage.core.Key;
+
+/**
+ * A {@link BinaryExpression} that corresponds to an algebraic comparison
+ * between a {@link Key} and a corresponding value.
+ * @param <T> - the type parameter of this expression's {@link Key}
+ */
+public final class BinaryComparisonExpression<T>
+        extends BinaryExpression<LiteralExpression<Key<T>>, LiteralExpression<T>, BinaryComparisonOperator> {
+    
+    /**
+     * Constructs a {@link BinaryComparisonExpression} whose operands are
+     * a {@link LiteralExpression} for a {@link Key} and a {@link LiteralExpression}
+     * for the value to compare against the key.
+     * <p>
+     * This constructor exists mainly for JSON serialization, use methods in
+     * {@link ExpressionFactory} instead of this constructor.
+     * @param leftOperand - left operand for this expression
+     * @param operator - the operator for this expression
+     * @param rightOperand - right operand for this expression
+     */
+    public BinaryComparisonExpression(LiteralExpression<Key<T>> leftOperand,
+            BinaryComparisonOperator operator, LiteralExpression<T> rightOperand) {
+        super(leftOperand, operator, rightOperand);
+    }
+    
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryComparisonOperator.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,55 @@
+/*
+ * 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.storage.query;
+
+/**
+ * Operators to be used with {@link BinaryComparisonExpression}
+ */
+public enum BinaryComparisonOperator implements BinaryOperator {
+    /** Equality comparison */
+    EQUALS,
+    /** Inequality comparison */
+    NOT_EQUAL_TO,
+    /** Greater than comparison */
+    GREATER_THAN,
+    /** Greater than or equal comparison */
+    GREATER_THAN_OR_EQUAL_TO,
+    /** Less than comparison */
+    LESS_THAN,
+    /** Less than or equal comparison */
+    LESS_THAN_OR_EQUAL_TO,
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryExpression.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,117 @@
+/*
+ * 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.storage.query;
+
+/**
+ * An {@link Expression} with two operands and one operator.
+ * @param <S> - the type of {@link Expression} corresponding to the left operand
+ * @param <T> - the type of {@link Expression} corresponding to the right operand
+ * @param <U> - the type of {@link BinaryOperator} corresponding to the operator
+ */
+abstract class BinaryExpression<S extends Expression, T extends Expression, U extends BinaryOperator>
+        implements Expression {
+    
+    private S leftOperand;
+    private U operator;
+    private T rightOperand;
+    
+    /**
+     * Constructs a {@link BinaryExpression} given two operands and an operator.
+     * <p>
+     * This constructor exists mainly for JSON serialization, use methods in
+     * {@link ExpressionFactory} instead of this constructor.
+     * @param leftOperand - left operand for this expression
+     * @param operator - the operator for this expression
+     * @param rightOperand - right operand for this expression
+     */
+    BinaryExpression(S leftOperand, U operator, T rightOperand) {
+        this.leftOperand = leftOperand;
+        this.operator = operator;
+        this.rightOperand = rightOperand;
+    }
+    
+    /**
+     * @return the left operand of this expression
+     */
+    public S getLeftOperand() {
+        return leftOperand;
+    }
+    
+    /**
+     * @return the operator of this expression
+     */
+    public U getOperator() {
+        return operator;
+    }
+    
+    /**
+     * @return the right operand of this expression
+     */
+    public T getRightOperand() {
+        return rightOperand;
+    }
+    
+    @Override
+    public boolean equals(Object obj) {
+        boolean result = false;
+        if (obj != null && obj instanceof BinaryExpression) {
+            BinaryExpression<?, ?, ?> otherExpr = (BinaryExpression<?, ?, ?>) obj;
+            result = leftOperand.equals(otherExpr.leftOperand)
+                    && rightOperand.equals(otherExpr.rightOperand)
+                    && operator.equals(otherExpr.operator);
+        }
+        return result;
+    }
+    
+    @Override
+    public int hashCode() {
+        return leftOperand.hashCode() + operator.hashCode() + rightOperand.hashCode();
+    }
+    
+    @Override
+    public String toString() {
+        StringBuffer buf = new StringBuffer();
+        buf.append("( ");
+        buf.append(leftOperand.toString());
+        buf.append(" ");
+        buf.append(operator.toString());
+        buf.append(" ");
+        buf.append(rightOperand.toString());
+        buf.append(" )");
+        return buf.toString();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryLogicalExpression.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,62 @@
+/*
+ * 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.storage.query;
+
+/**
+ * A {@link BinaryExpression} that represents a boolean formula
+ * with two expressions joined by a logical operator.
+ * @param <S> - the type of {@link Expression} corresponding to the left operand
+ * @param <T> - the type of {@link Expression} corresponding to the right operand
+ */
+public final class BinaryLogicalExpression<S extends Expression, T extends Expression>
+        extends BinaryExpression<S, T, BinaryLogicalOperator> {
+
+    /**
+     * Constructs a {@link BinaryLogicalExpression} whose operands are
+     * expressions joined by a {@link BinaryLogicalOperator}.
+     * <p>
+     * This constructor exists mainly for JSON serialization, use methods in
+     * {@link ExpressionFactory} instead of this constructor.
+     * @param leftOperand - left operand for this expression
+     * @param operator - the operator for this expression
+     * @param rightOperand - right operand for this expression
+     */
+    public BinaryLogicalExpression(S leftOperand, BinaryLogicalOperator operator, T rightOperand) {
+        super(leftOperand, operator, rightOperand);
+    }
+    
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryLogicalOperator.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,47 @@
+/*
+ * 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.storage.query;
+
+/**
+ * Boolean operators to be used with {@link BinaryLogicalExpression}.
+ */
+public enum BinaryLogicalOperator implements BinaryOperator {
+    /** Logical AND operation */
+    AND,
+    /** Logical OR operation */
+    OR,
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryOperator.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,44 @@
+/*
+ * 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.storage.query;
+
+/**
+ * Represents operators that take two operands.
+ */
+interface BinaryOperator extends Operator {
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/Expression.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,47 @@
+/*
+ * 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.storage.query;
+
+import com.redhat.thermostat.storage.core.Query;
+
+/**
+ * Base interface for all expressions used in storage queries.
+ * @see Query#where(Expression)
+ */
+public interface Expression {
+    
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/ExpressionFactory.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,153 @@
+/*
+ * 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.storage.query;
+
+import com.redhat.thermostat.storage.core.Key;
+import com.redhat.thermostat.storage.core.Query;
+
+/**
+ * This class provides convenience methods that should be used
+ * to create all expressions used in queries.
+ * 
+ * @see Expression
+ * @see Query#where(Expression)
+ */
+public class ExpressionFactory {
+    
+    /**
+     * Creates a {@link BinaryComparisonExpression} comparing the
+     * provided key and value for equality.
+     * @param key - {@link Key} whose value to compare against the provided value
+     * @param value - the value to compare against the key
+     * @return the new comparison expression
+     */
+    public <T> BinaryComparisonExpression<T> equalTo(Key<T> key, T value) {
+        return createComparisonExpression(key, value, BinaryComparisonOperator.EQUALS);
+    }
+    
+    /**
+     * Creates a {@link BinaryComparisonExpression} comparing the
+     * provided key and value for inequality.
+     * @param key - {@link Key} whose value to compare against the provided value
+     * @param value - the value to compare against the key
+     * @return the new comparison expression
+     */
+    public <T> BinaryComparisonExpression<T> notEqualTo(Key<T> key, T value) {
+        return createComparisonExpression(key, value, BinaryComparisonOperator.NOT_EQUAL_TO);
+    }
+    
+    /**
+     * Creates a {@link BinaryComparisonExpression} comparing if the
+     * provided key has a value greater than the provided value.
+     * @param key - {@link Key} whose value to compare against the provided value
+     * @param value - the value to compare against the key
+     * @return the new comparison expression
+     */
+    public <T> BinaryComparisonExpression<T> greaterThan(Key<T> key, T value) {
+        return createComparisonExpression(key, value, BinaryComparisonOperator.GREATER_THAN);
+    }
+    
+    /**
+     * Creates a {@link BinaryComparisonExpression} comparing if the
+     * provided key has a value less than the provided value.
+     * @param key - {@link Key} whose value to compare against the provided value
+     * @param value - the value to compare against the key
+     * @return the new comparison expression
+     */
+    public <T> BinaryComparisonExpression<T> lessThan(Key<T> key, T value) {
+        return createComparisonExpression(key, value, BinaryComparisonOperator.LESS_THAN);
+    }
+    
+    /**
+     * Creates a {@link BinaryComparisonExpression} comparing if the
+     * provided key has a value greater than or equal to the provided value.
+     * @param key - {@link Key} whose value to compare against the provided value
+     * @param value - the value to compare against the key
+     * @return the new comparison expression
+     */
+    public <T> BinaryComparisonExpression<T> greaterThanOrEqualTo(Key<T> key, T value) {
+        return createComparisonExpression(key, value, BinaryComparisonOperator.GREATER_THAN_OR_EQUAL_TO);
+    }
+    
+    /**
+     * Creates a {@link BinaryComparisonExpression} comparing if the
+     * provided key has a value less than or equal to the provided value.
+     * @param key - {@link Key} whose value to compare against the provided value
+     * @param value - the value to compare against the key
+     * @return the new comparison expression
+     */
+    public <T> BinaryComparisonExpression<T> lessThanOrEqualTo(Key<T> key, T value) {
+        return createComparisonExpression(key, value, BinaryComparisonOperator.LESS_THAN_OR_EQUAL_TO);
+    }
+    
+    /**
+     * Creates a {@link UnaryLogicalExpression} which is a logical
+     * negation of the provided expression.
+     * @param expr - the expression to negate
+     * @return the new negated expression
+     */
+    public <T extends Expression> UnaryLogicalExpression<T> not(T expr) {
+        return new UnaryLogicalExpression<>(expr, UnaryLogicalOperator.NOT);
+    }
+    
+    /**
+     * Creates a {@link BinaryLogicalExpression} with the two provided expressions
+     * joined in order by a logical AND operation.
+     * @param left - the left operand
+     * @param right - the right operand
+     * @return the new logical expression
+     */
+    public <S extends Expression, T extends Expression> BinaryLogicalExpression<S, T> and(S left, T right) {
+        return new BinaryLogicalExpression<S, T>(left, BinaryLogicalOperator.AND, right);
+    }
+    
+    /**
+     * Creates a {@link BinaryLogicalExpression} with the two provided expressions
+     * joined in order by a logical OR operation.
+     * @param left - the left operand
+     * @param right - the right operand
+     * @return the new logical expression
+     */
+    public <S extends Expression, T extends Expression> BinaryLogicalExpression<S, T> or(S left, T right) {
+        return new BinaryLogicalExpression<S, T>(left, BinaryLogicalOperator.OR, right);
+    }
+    
+    private <T> BinaryComparisonExpression<T> createComparisonExpression(Key<T> key, T value, BinaryComparisonOperator op) {
+        return new BinaryComparisonExpression<>(new LiteralExpression<>(key), op, new LiteralExpression<>(value));
+    }
+    
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/LiteralExpression.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,84 @@
+/*
+ * 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.storage.query;
+
+/**
+ * An {@link Expression} that contains a value.
+ * @param <T> - the type of this expression's value
+ */
+public final class LiteralExpression<T> implements Expression {
+
+    private T value;
+    
+    /**
+     * Constructs a {@link LiteralExpression} given a value.
+     * <p>
+     * This constructor exists mainly for JSON serialization, use methods in
+     * {@link ExpressionFactory} instead of this constructor.
+     * @param value - the value for this expression
+     */
+    public LiteralExpression(T value) {
+        this.value = value;
+    }
+    
+    /**
+     * @return the value represented by this expression
+     */
+    public T getValue() {
+        return value;
+    }
+    
+    @Override
+    public boolean equals(Object obj) {
+        boolean result = false;
+        if (obj != null && obj instanceof LiteralExpression) {
+            LiteralExpression<?> otherExpr = (LiteralExpression<?>) obj;
+            result = value.equals(otherExpr.value);
+        }
+        return result;
+    }
+    
+    @Override
+    public int hashCode() {
+        return value.hashCode();
+    }
+    
+    @Override
+    public String toString() {
+        return value.toString();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/Operator.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,45 @@
+/*
+ * 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.storage.query;
+
+/**
+ * Base interface for operators that are used with {@link Expression} objects.
+ * Concrete implementations must be enumeration types.
+ */
+public interface Operator {
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/UnaryExpression.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,102 @@
+/*
+ * 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.storage.query;
+
+/**
+ * An {@link Expression} that has an operator and one operand.
+ * @param <S> - type of {@link Expression} used for the operand
+ * @param <T> - type of {@link UnaryOperator} used for the operator
+ */
+abstract class UnaryExpression<S extends Expression, T extends UnaryOperator> implements Expression {
+    
+    private S operand;
+    private T operator;
+    
+    /**
+     * Constructs a {@link UnaryExpression} given an operand and an operator.
+     * <p>
+     * This constructor exists mainly for JSON serialization, use methods in
+     * {@link ExpressionFactory} instead of this constructor.
+     * @param operand - operand for this expression
+     * @param operator - operator for this expression
+     */
+    UnaryExpression(S operand, T operator) {
+        this.operand = operand;
+        this.operator = operator;
+    }
+    
+    /**
+     * @return the operand for this expression
+     */
+    public S getOperand() {
+        return operand;
+    }
+    
+    /**
+     * @return the operator for this expression
+     */
+    public T getOperator() {
+        return operator;
+    }
+    
+    @Override
+    public boolean equals(Object obj) {
+        boolean result = false;
+        if (obj != null && obj instanceof UnaryExpression) {
+            UnaryExpression<?, ?> otherExpr = (UnaryExpression<?, ?>) obj;
+            result = operand.equals(otherExpr.operand)
+                    && operator.equals(otherExpr.operator);
+        }
+        return result;
+    }
+    
+    @Override
+    public int hashCode() {
+        return operand.hashCode() + operator.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer buf = new StringBuffer();
+        buf.append("( ");
+        buf.append(operator.toString());
+        buf.append(" ");
+        buf.append(operand.toString());
+        buf.append(" )");
+        return buf.toString();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/UnaryLogicalExpression.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,61 @@
+/*
+ * 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.storage.query;
+
+/**
+ * A {@link UnaryExpression} which represents a boolean formula
+ * with one expression and a logical operator.
+ * @param <T> - type of {@link Expression} used for the operand
+ */
+public final class UnaryLogicalExpression<T extends Expression> extends
+        UnaryExpression<T, UnaryLogicalOperator> {
+
+    /**
+     * Constructs a {@link UnaryLogicalExpression} given an operand
+     * and a {@link UnaryLogicalOperator}.
+     * <p>
+     * This constructor exists mainly for JSON serialization, use methods in
+     * {@link ExpressionFactory} instead of this constructor.
+     * @param leftOperand - left operand for this expression
+     * @param operator - the operator for this expression
+     * @param rightOperand - right operand for this expression
+     */
+    public UnaryLogicalExpression(T operand, UnaryLogicalOperator operator) {
+        super(operand, operator);
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/UnaryLogicalOperator.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,45 @@
+/*
+ * 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.storage.query;
+
+/**
+ * Operators to be used with {@link UnaryLogicalExpression}.
+ */
+public enum UnaryLogicalOperator implements UnaryOperator {
+    /** Logical NOT operation */
+    NOT,
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/UnaryOperator.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,44 @@
+/*
+ * 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.storage.query;
+
+/**
+ * Represents operators that take one operand.
+ */
+interface UnaryOperator extends Operator {
+
+}
--- a/storage/core/src/test/java/com/redhat/thermostat/storage/core/HostLatestPojoListGetterTest.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/storage/core/src/test/java/com/redhat/thermostat/storage/core/HostLatestPojoListGetterTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -40,6 +40,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -52,13 +53,9 @@
 import org.junit.Before;
 import org.junit.Test;
 
-import com.redhat.thermostat.storage.core.Category;
-import com.redhat.thermostat.storage.core.Cursor;
-import com.redhat.thermostat.storage.core.Key;
-import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
-import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.model.TimeStampedPojo;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 
 public class HostLatestPojoListGetterTest {
     private static final String AGENT_ID = "agentid";
@@ -115,8 +112,8 @@
 
         assertNotNull(query);
         verify(storage).createQuery(cat);
-        verify(query).where(Key.TIMESTAMP, Criteria.GREATER_THAN, 123l);
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, AGENT_ID);
+        Expression expr = createWhereExpression(123l);
+        verify(query).where(eq(expr));
         verify(query).sort(Key.TIMESTAMP, Query.SortDirection.DESCENDING);
         verifyNoMoreInteractions(query);
     }
@@ -135,8 +132,8 @@
 
         assertNotNull(query);
         verify(storage, times(2)).createQuery(cat);
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, AGENT_ID);
-        verify(query).where(Key.TIMESTAMP, Criteria.GREATER_THAN, Long.MIN_VALUE);
+        Expression expr = createWhereExpression(Long.MIN_VALUE);
+        verify(query).where(expr);
         verify(query).sort(Key.TIMESTAMP, Query.SortDirection.DESCENDING);
         verifyNoMoreInteractions(query);
     }
@@ -157,8 +154,8 @@
 
         List<TestPojo> stats = getter.getLatest(ref, Long.MIN_VALUE);
 
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, AGENT_ID);
-        verify(query).where(Key.TIMESTAMP, Criteria.GREATER_THAN, Long.MIN_VALUE);
+        Expression expr = createWhereExpression(Long.MIN_VALUE);
+        verify(query).where(expr);
 
         assertNotNull(stats);
         assertEquals(2, stats.size());
@@ -170,6 +167,11 @@
         assertArrayEquals(new double[] {load5_2, load10_2, load15_2}, stat2.getData(), 0.001);
     }
 
+    private Expression createWhereExpression(long time) {
+        ExpressionFactory factory = new ExpressionFactory();
+        return factory.and(factory.equalTo(Key.AGENT_ID, AGENT_ID), factory.greaterThan(Key.TIMESTAMP, time));
+    }
+
     @After
     public void tearDown() {
         ref = null;
--- a/storage/core/src/test/java/com/redhat/thermostat/storage/core/VmLatestPojoListGetterTest.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/storage/core/src/test/java/com/redhat/thermostat/storage/core/VmLatestPojoListGetterTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -39,6 +39,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -50,13 +51,9 @@
 import org.junit.Before;
 import org.junit.Test;
 
-import com.redhat.thermostat.storage.core.Category;
-import com.redhat.thermostat.storage.core.Cursor;
-import com.redhat.thermostat.storage.core.Key;
-import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
-import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.model.TimeStampedPojo;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 
 public class VmLatestPojoListGetterTest {
     private static final String AGENT_ID = "agentid";
@@ -109,13 +106,20 @@
 
         assertNotNull(query);
         verify(storage).createQuery(cat);
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, AGENT_ID);
-        verify(query).where(Key.VM_ID, Criteria.EQUALS, VM_PID);
-        verify(query).where(Key.TIMESTAMP, Criteria.GREATER_THAN, 123l);
+        Expression expr = createWhereExpression(123l);
+        verify(query).where(eq(expr));
         verify(query).sort(Key.TIMESTAMP, Query.SortDirection.DESCENDING);
         verifyNoMoreInteractions(query);
     }
 
+    private Expression createWhereExpression(long time) {
+        ExpressionFactory factory = new ExpressionFactory();
+        return factory.and(
+                factory.equalTo(Key.AGENT_ID, AGENT_ID),
+                factory.and(factory.equalTo(Key.VM_ID, VM_PID),
+                        factory.greaterThan(Key.TIMESTAMP, time)));
+    }
+
     @Test
     public void testBuildQueryPopulatesUpdateTimes() {
         Storage storage = mock(Storage.class);
@@ -129,9 +133,8 @@
 
         assertNotNull(query);
         verify(storage, times(2)).createQuery(cat);
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, AGENT_ID);
-        verify(query).where(Key.VM_ID, Criteria.EQUALS, VM_PID);
-        verify(query).where(Key.TIMESTAMP, Criteria.GREATER_THAN, Long.MIN_VALUE);
+        Expression expr = createWhereExpression(Long.MIN_VALUE);
+        verify(query).where(eq(expr));
         verify(query).sort(Key.TIMESTAMP, Query.SortDirection.DESCENDING);
         verifyNoMoreInteractions(query);
     }
@@ -153,9 +156,8 @@
         List<TestPojo> stats = getter.getLatest(vmRef, t2);
 
         verify(storage).createQuery(cat);
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, AGENT_ID);
-        verify(query).where(Key.VM_ID, Criteria.EQUALS, VM_PID);
-        verify(query).where(Key.TIMESTAMP, Criteria.GREATER_THAN, t2);
+        Expression expr = createWhereExpression(t2);
+        verify(query).where(eq(expr));
         verify(query).sort(Key.TIMESTAMP, Query.SortDirection.DESCENDING);
         verify(query).execute();
         verifyNoMoreInteractions(query);
--- a/storage/core/src/test/java/com/redhat/thermostat/storage/internal/dao/AgentInfoDAOTest.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/storage/core/src/test/java/com/redhat/thermostat/storage/internal/dao/AgentInfoDAOTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -40,6 +40,7 @@
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 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.verifyNoMoreInteractions;
@@ -56,13 +57,14 @@
 import com.redhat.thermostat.storage.core.HostRef;
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Remove;
 import com.redhat.thermostat.storage.core.Replace;
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.core.Update;
 import com.redhat.thermostat.storage.dao.AgentInfoDAO;
 import com.redhat.thermostat.storage.model.AgentInformation;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 
 public class AgentInfoDAOTest {
 
@@ -150,7 +152,9 @@
         List<AgentInformation> aliveAgents = dao.getAliveAgents();
 
         verify(storage).createQuery(AgentInfoDAO.CATEGORY);
-        verify(query).where(AgentInfoDAO.ALIVE_KEY, Criteria.EQUALS, true);
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.equalTo(AgentInfoDAO.ALIVE_KEY, Boolean.TRUE);
+        verify(query).where(eq(expr));
         verify(query).execute();
         verifyNoMoreInteractions(query);
 
@@ -198,7 +202,9 @@
         AgentInformation computed = dao.getAgentInformation(agentRef);
 
         verify(storage).createQuery(AgentInfoDAO.CATEGORY);
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, agentInfo1.getAgentId());
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.equalTo(Key.AGENT_ID, agentInfo1.getAgentId());
+        verify(query).where(eq(expr));
         verify(query).limit(1);
         verify(query).execute();
         verifyNoMoreInteractions(query);
--- a/storage/core/src/test/java/com/redhat/thermostat/storage/internal/dao/BackendInfoDAOTest.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/storage/core/src/test/java/com/redhat/thermostat/storage/internal/dao/BackendInfoDAOTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -39,6 +39,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -59,11 +60,15 @@
 import com.redhat.thermostat.storage.core.HostRef;
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Remove;
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.dao.BackendInfoDAO;
 import com.redhat.thermostat.storage.model.BackendInformation;
+import com.redhat.thermostat.storage.query.BinaryComparisonExpression;
+import com.redhat.thermostat.storage.query.BinaryComparisonOperator;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
+import com.redhat.thermostat.storage.query.LiteralExpression;
 
 public class BackendInfoDAOTest {
 
@@ -147,7 +152,9 @@
         List<BackendInformation> result = dao.getBackendInformation(agentref);
 
         verify(storage).createQuery(BackendInfoDAO.CATEGORY);
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, AGENT_ID);
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.equalTo(Key.AGENT_ID, AGENT_ID);
+        verify(query).where(eq(expr));
         verify(query).execute();
         verifyNoMoreInteractions(query);
 
--- a/storage/core/src/test/java/com/redhat/thermostat/storage/internal/dao/NetworkInterfaceInfoDAOTest.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/storage/core/src/test/java/com/redhat/thermostat/storage/internal/dao/NetworkInterfaceInfoDAOTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -39,6 +39,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 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.verifyNoMoreInteractions;
@@ -54,11 +55,15 @@
 import com.redhat.thermostat.storage.core.HostRef;
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Replace;
 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.BinaryComparisonExpression;
+import com.redhat.thermostat.storage.query.BinaryComparisonOperator;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
+import com.redhat.thermostat.storage.query.LiteralExpression;
 
 public class NetworkInterfaceInfoDAOTest {
 
@@ -102,7 +107,9 @@
         NetworkInterfaceInfoDAO dao = new NetworkInterfaceInfoDAOImpl(storage);
         List<NetworkInterfaceInfo> netInfo = dao.getNetworkInterfaces(hostRef);
 
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, "system");
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.equalTo(Key.AGENT_ID, "system");
+        verify(query).where(eq(expr));
         verify(query).execute();
         verifyNoMoreInteractions(query);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/test/java/com/redhat/thermostat/storage/query/BinaryExpressionTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,113 @@
+/*
+ * 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.storage.query;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.mockito.Mockito.mock;
+
+import org.junit.Before;
+import org.junit.Test;
+
+
+public class BinaryExpressionTest {
+    
+    private Expression left;
+    private Expression right;
+    private BinaryOperator op;
+    private BinaryExpression<Expression, Expression, BinaryOperator> expr;
+
+    @Before
+    public void setup() {
+        left = mock(Expression.class);
+        right = mock(Expression.class);
+        op = mock(BinaryOperator.class);
+        expr = new BinaryExpression<Expression, Expression, BinaryOperator>(
+                left, op, right) {
+        };
+    }
+    
+    @Test
+    public void testGetLeftOperand() {
+        assertEquals(left, expr.getLeftOperand());
+    }
+    
+    @Test
+    public void testGetRightOperand() {
+        assertEquals(right, expr.getRightOperand());
+    }
+    
+    @Test
+    public void testGetOperator() {
+        assertEquals(op, expr.getOperator());
+    }
+    
+    @Test
+    public void testEquals() {
+        BinaryExpression<Expression, Expression, BinaryOperator> otherExpr = new BinaryExpression<Expression, Expression, BinaryOperator>(
+                left, op, right) {
+        };
+        assertEquals(expr, otherExpr);
+    }
+    
+    @Test
+    public void testNotEquals() {
+        Expression otherLeft = mock(Expression.class);
+        BinaryExpression<Expression, Expression, BinaryOperator> otherExpr = new BinaryExpression<Expression, Expression, BinaryOperator>(
+                otherLeft, op, right) {
+        };
+        
+        assertFalse(expr.equals(otherExpr));
+    }
+    
+    @Test
+    public void testNotEqualsWrongClass() {
+        assertFalse(expr.equals(new Object()));
+    }
+    
+    @Test
+    public void testNotEqualsNull() {
+        assertFalse(expr.equals(null));
+    }
+    
+    @Test
+    public void testHashCode() {
+        int hashCode = left.hashCode() + right.hashCode() + op.hashCode();
+        assertEquals(hashCode, expr.hashCode());
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/test/java/com/redhat/thermostat/storage/query/ExpressionFactoryTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,129 @@
+/*
+ * 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.storage.query;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.redhat.thermostat.storage.core.Key;
+
+public class ExpressionFactoryTest {
+    
+    private static final Key<String> key = new Key<>("hello", true);
+    private static final String VALUE = "world";
+    ExpressionFactory factory;
+
+    @Before
+    public void setUp() throws Exception {
+        factory = new ExpressionFactory();
+    }
+
+    @Test
+    public void testEqualTo() {
+        Expression expr = new BinaryComparisonExpression<>(
+                new LiteralExpression<>(key), BinaryComparisonOperator.EQUALS,
+                new LiteralExpression<>(VALUE));
+        assertEquals(expr, factory.equalTo(key, VALUE));
+    }
+
+    @Test
+    public void testNotEqualTo() {
+        Expression expr = new BinaryComparisonExpression<>(
+                new LiteralExpression<>(key), BinaryComparisonOperator.NOT_EQUAL_TO,
+                new LiteralExpression<>(VALUE));
+        assertEquals(expr, factory.notEqualTo(key, VALUE));
+    }
+
+    @Test
+    public void testGreaterThan() {
+        Expression expr = new BinaryComparisonExpression<>(
+                new LiteralExpression<>(key), BinaryComparisonOperator.GREATER_THAN,
+                new LiteralExpression<>(VALUE));
+        assertEquals(expr, factory.greaterThan(key, VALUE));
+    }
+
+    @Test
+    public void testLessThan() {
+        Expression expr = new BinaryComparisonExpression<>(
+                new LiteralExpression<>(key), BinaryComparisonOperator.LESS_THAN,
+                new LiteralExpression<>(VALUE));
+        assertEquals(expr, factory.lessThan(key, VALUE));
+    }
+
+    @Test
+    public void testGreaterThanOrEqualTo() {
+        Expression expr = new BinaryComparisonExpression<>(
+                new LiteralExpression<>(key), BinaryComparisonOperator.GREATER_THAN_OR_EQUAL_TO,
+                new LiteralExpression<>(VALUE));
+        assertEquals(expr, factory.greaterThanOrEqualTo(key, VALUE));
+    }
+
+    @Test
+    public void testLessThanOrEqualTo() {
+        Expression expr = new BinaryComparisonExpression<>(
+                new LiteralExpression<>(key), BinaryComparisonOperator.LESS_THAN_OR_EQUAL_TO,
+                new LiteralExpression<>(VALUE));
+        assertEquals(expr, factory.lessThanOrEqualTo(key, VALUE));
+    }
+
+    @Test
+    public void testNot() {
+        Expression operand = mock(Expression.class);
+        Expression expr = new UnaryLogicalExpression<Expression>(operand, UnaryLogicalOperator.NOT);
+        assertEquals(expr, factory.not(operand));
+    }
+
+    @Test
+    public void testAnd() {
+        Expression left = mock(Expression.class);
+        Expression right = mock(Expression.class);
+        Expression expr = new BinaryLogicalExpression<>(left, BinaryLogicalOperator.AND, right);
+        assertEquals(expr, factory.and(left, right));
+    }
+
+    @Test
+    public void testOr() {
+        Expression left = mock(Expression.class);
+        Expression right = mock(Expression.class);
+        Expression expr = new BinaryLogicalExpression<>(left, BinaryLogicalOperator.OR, right);
+        assertEquals(expr, factory.or(left, right));
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/test/java/com/redhat/thermostat/storage/query/LiteralExpressionTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,89 @@
+/*
+ * 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.storage.query;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class LiteralExpressionTest {
+
+    private Object value;
+    private LiteralExpression<Object> expr;
+
+    @Before
+    public void setup() {
+        value = new Object();
+        expr = new LiteralExpression<Object>(value);
+    }
+    
+    @Test
+    public void testGetValue() {
+        assertEquals(value, expr.getValue());
+    }
+    
+    @Test
+    public void testEquals() {
+        LiteralExpression<Object> otherExpr = new LiteralExpression<Object>(value);
+        assertEquals(expr, otherExpr);
+    }
+    
+    @Test
+    public void testNotEquals() {
+        Object otherValue = new Object();
+        LiteralExpression<Object> otherExpr = new LiteralExpression<Object>(otherValue);
+        
+        assertFalse(expr.equals(otherExpr));
+    }
+    
+    @Test
+    public void testNotEqualsWrongClass() {
+        assertFalse(expr.equals(new Object()));
+    }
+    
+    @Test
+    public void testNotEqualsNull() {
+        assertFalse(expr.equals(null));
+    }
+    
+    @Test
+    public void testHashCode() {
+        assertEquals(value.hashCode(), expr.hashCode());
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/test/java/com/redhat/thermostat/storage/query/UnaryExpressionTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,106 @@
+/*
+ * 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.storage.query;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.mockito.Mockito.mock;
+
+import org.junit.Before;
+import org.junit.Test;
+
+
+public class UnaryExpressionTest {
+    
+    private Expression operand;
+    private UnaryOperator operator;
+    private UnaryExpression<Expression, UnaryOperator> expr;
+
+    @Before
+    public void setup() {
+        operand = mock(Expression.class);
+        operator = mock(UnaryOperator.class);
+        expr = new UnaryExpression<Expression, UnaryOperator>(
+                operand, operator) {
+        };
+    }
+    
+    @Test
+    public void testGetOperand() {
+        assertEquals(operand, expr.getOperand());
+    }
+    
+    @Test
+    public void testGetOperator() {
+        assertEquals(operator, expr.getOperator());
+    }
+    
+    @Test
+    public void testEquals() {
+        UnaryExpression<Expression, UnaryOperator> otherExpr = new UnaryExpression<Expression, UnaryOperator>(
+                operand, operator) {
+        };
+        assertEquals(expr, otherExpr);
+    }
+    
+    @Test
+    public void testNotEquals() {
+        Expression otherOperand = mock(Expression.class);
+        UnaryExpression<Expression, UnaryOperator> otherExpr = new UnaryExpression<Expression, UnaryOperator>(
+                otherOperand, operator) {
+        };
+        
+        assertFalse(expr.equals(otherExpr));
+    }
+    
+    @Test
+    public void testNotEqualsWrongClass() {
+        assertFalse(expr.equals(new Object()));
+    }
+    
+    @Test
+    public void testNotEqualsNull() {
+        assertFalse(expr.equals(null));
+    }
+    
+    @Test
+    public void testHashCode() {
+        int hashCode = operator.hashCode() + operand.hashCode();
+        assertEquals(hashCode, expr.hashCode());
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/MongoExpressionParser.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,181 @@
+/*
+ * 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.storage.mongodb.internal;
+
+import java.util.Set;
+
+import com.mongodb.BasicDBList;
+import com.mongodb.BasicDBObject;
+import com.mongodb.BasicDBObjectBuilder;
+import com.mongodb.DBObject;
+import com.redhat.thermostat.storage.core.Key;
+import com.redhat.thermostat.storage.query.BinaryComparisonExpression;
+import com.redhat.thermostat.storage.query.BinaryComparisonOperator;
+import com.redhat.thermostat.storage.query.BinaryLogicalExpression;
+import com.redhat.thermostat.storage.query.BinaryLogicalOperator;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.LiteralExpression;
+import com.redhat.thermostat.storage.query.UnaryLogicalExpression;
+import com.redhat.thermostat.storage.query.UnaryLogicalOperator;
+
+public class MongoExpressionParser {
+    
+    public DBObject parse(Expression expr) {
+        DBObject result;
+        if (expr instanceof BinaryComparisonExpression) {
+            result = parseBinaryLiteralExpression((BinaryComparisonExpression<?>) expr);
+        }
+        else if (expr instanceof BinaryLogicalExpression) {
+            // LHS OP RHS ==> { OP : [ LHS, RHS ] }
+            BinaryLogicalExpression<?, ?> biExpr = (BinaryLogicalExpression<?, ?>) expr;
+            BinaryLogicalOperator op = biExpr.getOperator();
+            
+            DBObject leftResult = parse(biExpr.getLeftOperand());
+            DBObject rightResult = parse(biExpr.getRightOperand());
+            BasicDBList list = new BasicDBList();
+            list.add(leftResult);
+            list.add(rightResult);
+            
+            result = new BasicDBObject(getLogicalOperator(op), list);
+        }
+        else if (expr instanceof UnaryLogicalExpression) {
+            UnaryLogicalExpression<?> uniExpr = (UnaryLogicalExpression<?>) expr;
+            Expression operand = uniExpr.getOperand();
+            UnaryLogicalOperator op = uniExpr.getOperator();
+            String strOp = getLogicalOperator(op);
+            // In MongoDB, NOT can only be applied to negate comparisons
+            if (operand instanceof BinaryComparisonExpression) {
+                BinaryComparisonExpression<?> binExpr = (BinaryComparisonExpression<?>) operand;
+                if (binExpr.getOperator() == BinaryComparisonOperator.EQUALS) {
+                    throw new IllegalArgumentException("Cannot use $not with equality");
+                }
+                DBObject object = parseBinaryLiteralExpression(binExpr);
+                // OP { LHS : RHS } => { LHS : { OP : RHS }}
+                // Apply operator to each key
+                Set<String> keySet = object.keySet();
+                for (String key : keySet) {
+                    Object value = object.get(key);
+                    BasicDBObject newValue = new BasicDBObject(strOp, value);
+                    object.put(key, newValue);
+                }
+                
+                result = object;
+            }
+            else {
+                throw new IllegalArgumentException("Not a valid use of " + strOp + " in MongoDB");
+            }
+        }
+        else {
+            throw new IllegalArgumentException("Unknown Expression of type " + expr.getClass());
+        }
+        
+        return result;
+    }
+
+    private <T> DBObject parseBinaryLiteralExpression(BinaryComparisonExpression<T> expr) {
+        BasicDBObjectBuilder builder = BasicDBObjectBuilder.start();
+        LiteralExpression<Key<T>> leftExpr = expr.getLeftOperand();
+        LiteralExpression<T> rightExpr = expr.getRightOperand();
+        BinaryComparisonOperator op = expr.getOperator();
+        if (op == BinaryComparisonOperator.EQUALS) {
+            // LHS == RHS => { LHS : RHS }
+            builder.add(leftExpr.getValue().getName(), rightExpr.getValue());
+        }
+        else {
+            // LHS OP RHS => { LHS : { OP : RHS } }
+            builder.push(leftExpr.getValue().getName());
+
+            String mongoOp = getLiteralOperator(op);
+            builder.add(mongoOp, rightExpr.getValue());
+
+            builder.pop();
+        }
+        return builder.get();
+    }
+    
+    private String getLiteralOperator(BinaryComparisonOperator operator) {
+        String result;
+        switch (operator) {
+        case NOT_EQUAL_TO:
+            result = "$ne";
+            break;
+        case LESS_THAN:
+            result = "$lt";
+            break;
+        case LESS_THAN_OR_EQUAL_TO:
+            result = "$lte";
+            break;
+        case GREATER_THAN:
+            result = "$gt";
+            break;
+        case GREATER_THAN_OR_EQUAL_TO:
+            result = "$gte";
+            break;
+        default:
+            throw new IllegalArgumentException("MongoQuery can not handle " + operator);
+        }
+        return result;
+    }
+    
+    private String getLogicalOperator(BinaryLogicalOperator operator) {
+        String result;
+        switch (operator) {
+        case AND:
+            result = "$and";
+            break;
+        case OR:
+            result = "$or";
+            break;
+        default:
+            throw new IllegalArgumentException("MongoQuery can not handle " + operator);
+        }
+        return result;
+    }
+    
+    private String getLogicalOperator(UnaryLogicalOperator operator) {
+        String result;
+        switch (operator) {
+        case NOT:
+            result = "$not";
+            break;
+        default:
+            throw new IllegalArgumentException("MongoQuery can not handle " + operator);
+        }
+        return result;
+    }
+
+}
--- a/storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/MongoQuery.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/MongoQuery.java	Fri Jun 07 13:49:04 2013 -0400
@@ -36,35 +36,35 @@
 
 package com.redhat.thermostat.storage.mongodb.internal;
 
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 
 import com.mongodb.BasicDBObject;
-import com.mongodb.BasicDBObjectBuilder;
 import com.mongodb.DBObject;
 import com.redhat.thermostat.storage.core.AbstractQuery;
 import com.redhat.thermostat.storage.core.Category;
 import com.redhat.thermostat.storage.core.Cursor;
-import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.model.Pojo;
+import com.redhat.thermostat.storage.query.Expression;
 
 public class MongoQuery<T extends Pojo> extends AbstractQuery<T> {
 
     private MongoStorage storage;
-    private BasicDBObject query = new BasicDBObject();
-    private Map<String, BasicDBObjectBuilder> builerMap;
+    private DBObject query = new BasicDBObject();
     
     private boolean hasClauses = false;
     private Category<T> category;
     private Class<T> resultClass;
+    private MongoExpressionParser parser;
 
     MongoQuery(MongoStorage storage, Category<T> category) {
+        this(storage, category, new MongoExpressionParser());
+    }
+    
+    MongoQuery(MongoStorage storage, Category<T> category, MongoExpressionParser parser) {
         this.storage = storage;
         this.category = category;
+        this.parser = parser;
         this.resultClass = category.getDataClass();
-        this.builerMap = new HashMap<String, BasicDBObjectBuilder>();
     }
 
     public Category<T> getCategory() {
@@ -76,51 +76,8 @@
     }
 
     @Override
-    public <S> void where(Key<S> key, Criteria operator, S value) {
-        where(key.getName(), operator, value);
-    }
-
-    public void where(String key, Criteria operator, Object value) {
-
-        // strict equality is mutually exclusive on the key
-        if (operator.equals(Criteria.EQUALS)) {
-            query.put(key, value);
-        
-        } else {
-            BasicDBObjectBuilder queryParameters = null;
-            if (builerMap.containsKey(key)) {
-                queryParameters = (BasicDBObjectBuilder) builerMap.get(key);
-            } else {
-                queryParameters = BasicDBObjectBuilder.start();
-                builerMap.put(key, queryParameters);
-            }
-            
-            switch (operator) {
-    
-            case NOT_EQUAL_TO:
-                queryParameters.add("$ne", value);
-                break;
-    
-            case LESS_THAN:
-                queryParameters.add("$lt", value);
-                break;
-    
-            case LESS_THAN_OR_EQUAL_TO:
-                queryParameters.add("$lte", value);
-                break;
-            case GREATER_THAN:
-                queryParameters.add("$gt", value);
-                break;
-    
-            case GREATER_THAN_OR_EQUAL_TO:
-                queryParameters.add("$gte", value);
-                break;
-    
-            default:
-                throw new IllegalArgumentException("MongoQuery can not handle " + operator);
-            }
-            query.put(key, queryParameters.get());
-        }
+    public void where(Expression expr) {
+        query = parser.parse(expr);
         hasClauses = true;
     }
 
@@ -156,5 +113,6 @@
     public Cursor<T> execute() {
         return storage.findAllPojos(this, resultClass);
     }
+
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoExpressionParserTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,214 @@
+/*
+ * 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.storage.mongodb.internal;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.mongodb.BasicDBList;
+import com.mongodb.BasicDBObjectBuilder;
+import com.mongodb.DBObject;
+import com.redhat.thermostat.storage.core.Key;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
+
+public class MongoExpressionParserTest {
+    
+    private final Key<Integer> KEY_1 = new Key<>("test", true);
+    private final Key<Integer> KEY_2 = new Key<>("test2", true);
+    private final Key<String> KEY_3 = new Key<>("key", true);
+    
+    private MongoExpressionParser parser;
+
+    @Before
+    public void setUp() throws Exception {
+        parser = new MongoExpressionParser();
+    }
+
+    @Test
+    public void testWhereEquals() {
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.equalTo(KEY_3, "value");
+        DBObject query = BasicDBObjectBuilder.start().add(KEY_3.getName(), "value").get();
+        assertEquals(query, parser.parse(expr));
+    }
+
+    @Test
+    public void testWhereNotEquals() {
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.notEqualTo(KEY_3, "value");
+        DBObject query = BasicDBObjectBuilder.start().push(KEY_3.getName()).add("$ne", "value").get();
+        assertEquals(query, parser.parse(expr));
+    }
+
+    @Test
+    public void testWhereGreaterThan() {
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.greaterThan(KEY_3, "value");
+        DBObject query = BasicDBObjectBuilder.start().push(KEY_3.getName()).add("$gt", "value").get();
+        assertEquals(query, parser.parse(expr));
+    }
+
+    @Test
+    public void testWhereGreaterThanOrEqualTo() {
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.greaterThanOrEqualTo(KEY_3, "value");
+        DBObject query = BasicDBObjectBuilder.start().push(KEY_3.getName()).add("$gte", "value").get();
+        assertEquals(query, parser.parse(expr));
+    }
+
+    @Test
+    public void testWhereLessThan() {
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.lessThan(KEY_3, "value");
+        DBObject query = BasicDBObjectBuilder.start().push(KEY_3.getName()).add("$lt", "value").get();
+        assertEquals(query, parser.parse(expr));
+    }
+
+    @Test
+    public void testWhereLessThanOrEqualTo() {
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.lessThanOrEqualTo(KEY_3, "value");
+        DBObject query = BasicDBObjectBuilder.start().push(KEY_3.getName()).add("$lte", "value").get();
+        assertEquals(query, parser.parse(expr));
+    }
+
+    @Test
+    public void testMultiWhere() {
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(factory.lessThanOrEqualTo(KEY_1, 1), factory.greaterThan(KEY_1, 2));
+        
+        BasicDBList list = new BasicDBList();
+        list.add(BasicDBObjectBuilder.start().push("test").add("$lte", 1).get());
+        list.add(BasicDBObjectBuilder.start().push("test").add("$gt", 2).get());
+        DBObject dbObject = BasicDBObjectBuilder.start().add("$and", list).get();
+        assertEquals(dbObject, parser.parse(expr));
+    }
+    
+    @Test
+    public void testMultiWhere2() {
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(factory.lessThanOrEqualTo(KEY_1, 1), factory.greaterThan(KEY_2, 2));
+
+        BasicDBList list = new BasicDBList();
+        list.add(BasicDBObjectBuilder.start().push("test").add("$lte", 1).get());
+        list.add(BasicDBObjectBuilder.start().push("test2").add("$gt", 2).get());
+        DBObject dbObject = BasicDBObjectBuilder.start().add("$and", list).get();
+        assertEquals(dbObject, parser.parse(expr));
+    }
+    
+    @Test
+    public void testMultiWhere3() {
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(factory.equalTo(KEY_1, 1), factory.greaterThan(KEY_1, 2));
+
+        BasicDBList list = new BasicDBList();
+        list.add(BasicDBObjectBuilder.start().add("test", 1).get());
+        list.add(BasicDBObjectBuilder.start().push("test").add("$gt", 2).get());
+        DBObject dbObject = BasicDBObjectBuilder.start().add("$and", list).get();
+        assertEquals(dbObject, parser.parse(expr));
+    }
+    
+    @Test
+    public void testMultiWhere4() {
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(factory.equalTo(KEY_1, 1), factory.greaterThan(KEY_2, 2));
+
+        BasicDBList list = new BasicDBList();
+        list.add(BasicDBObjectBuilder.start().add("test", 1).get());
+        list.add(BasicDBObjectBuilder.start().push("test2").add("$gt", 2).get());
+        DBObject dbObject = BasicDBObjectBuilder.start().add("$and", list).get();
+        assertEquals(dbObject, parser.parse(expr));
+    }
+    
+    @Test
+    public void testWhereOr() {
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.or(factory.equalTo(KEY_1, 1), factory.greaterThan(KEY_2, 2));
+
+        BasicDBList list = new BasicDBList();
+        list.add(BasicDBObjectBuilder.start().add("test", 1).get());
+        list.add(BasicDBObjectBuilder.start().push("test2").add("$gt", 2).get());
+        DBObject dbObject = BasicDBObjectBuilder.start().add("$or", list).get();
+        assertEquals(dbObject, parser.parse(expr));
+    }
+    
+    @Test
+    public void testWhereNot() {
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.not(factory.greaterThan(KEY_1, 1));
+
+        DBObject dbObject = BasicDBObjectBuilder.start().push("test").push("$not").add("$gt", 1).get();
+        assertEquals(dbObject, parser.parse(expr));
+    }
+    
+    @Test(expected=IllegalArgumentException.class)
+    public void testWhereNotLogicalExpr() {
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.not(factory.and(factory.equalTo(KEY_1, 1), factory.equalTo(KEY_2, 2)));
+        parser.parse(expr);
+    }
+    
+    @Test(expected=IllegalArgumentException.class)
+    public void testWhereLogicalNotEquals() {
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.not(factory.equalTo(KEY_1, 1));
+        parser.parse(expr);
+    }
+    
+    @Test
+    public void testWhere3() {
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(
+                factory.lessThanOrEqualTo(KEY_3, "value"),
+                factory.and(factory.equalTo(KEY_1, 1),
+                        factory.greaterThan(KEY_2, 2)));
+
+        BasicDBList list = new BasicDBList();
+        list.add(BasicDBObjectBuilder.start().add("test", 1).get());
+        list.add(BasicDBObjectBuilder.start().push("test2").add("$gt", 2).get());
+        DBObject dbObject = BasicDBObjectBuilder.start().add("$and", list).get();
+        list = new BasicDBList();
+        list.add(BasicDBObjectBuilder.start().push("key").add("$lte", "value").get());
+        list.add(dbObject);
+        dbObject = BasicDBObjectBuilder.start().add("$and", list).get();
+        assertEquals(dbObject, parser.parse(expr));
+    }
+
+}
--- a/storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoQueryTest.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoQueryTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -37,138 +37,72 @@
 package com.redhat.thermostat.storage.mongodb.internal;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
 import com.mongodb.BasicDBObject;
-import com.mongodb.BasicDBObjectBuilder;
 import com.mongodb.DBObject;
 import com.redhat.thermostat.storage.core.Category;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.model.Pojo;
+import com.redhat.thermostat.storage.query.Expression;
 
 public class MongoQueryTest {
-
+    
     private static class TestClass implements Pojo {
         
     }
 
     private static MongoStorage storage;
     private static Category<TestClass> category;
+    private static MongoExpressionParser parser;
+    private static DBObject dbObject;
 
     @BeforeClass
     public static void setUp() {
         storage = mock(MongoStorage.class);
         category = new Category<>("some-collection", TestClass.class);
+        parser = mock(MongoExpressionParser.class);
+        dbObject = new BasicDBObject();
+        when(parser.parse(any(Expression.class))).thenReturn(dbObject);
     }
 
     @AfterClass
     public static void tearDown() {
         storage = null;
         category = null;
+        parser = null;
     }
 
     @Test
     public void testEmptyQuery() {
-        
-        MongoQuery<TestClass> query = new MongoQuery<>(storage, category);
+        MongoQuery<TestClass> query = new MongoQuery<>(storage, category, parser);
         DBObject mongoQuery = query.getGeneratedQuery();
+        assertFalse(query.hasClauses());
         assertTrue(mongoQuery.keySet().isEmpty());
     }
 
     @Test
     public void testCollectionName() {
-        MongoQuery<TestClass> query = new MongoQuery<>(storage, category);
+        MongoQuery<TestClass> query = new MongoQuery<>(storage, category, parser);
         assertEquals("some-collection", query.getCategory().getName());
     }
-
-    @Test
-    public void testWhereEquals() {
-        DBObject generatedQuery = generateSimpleWhereQuery("key", Criteria.EQUALS, "value");
-        assertEquals("value", generatedQuery.get("key"));
-    }
-
-    @Test
-    public void testWhereNotEquals() {
-        DBObject generatedQuery = generateSimpleWhereQuery("key", Criteria.NOT_EQUAL_TO, "value");
-        assertEquals(new BasicDBObject("$ne", "value"), generatedQuery.get("key"));
-    }
-
-    @Test
-    public void testWhereGreaterThan() {
-        DBObject generatedQuery = generateSimpleWhereQuery("key", Criteria.GREATER_THAN, "value");
-        assertEquals(new BasicDBObject("$gt", "value"), generatedQuery.get("key"));
-    }
-
-    @Test
-    public void testWhereGreaterThanOrEqualTo() {
-        DBObject generatedQuery = generateSimpleWhereQuery("key", Criteria.GREATER_THAN_OR_EQUAL_TO, "value");
-        assertEquals(new BasicDBObject("$gte", "value"), generatedQuery.get("key"));
-    }
-
-    @Test
-    public void testWhereLessThan() {
-        DBObject generatedQuery = generateSimpleWhereQuery("key", Criteria.LESS_THAN, "value");
-        assertEquals(new BasicDBObject("$lt", "value"), generatedQuery.get("key"));
-    }
-
-    @Test
-    public void testWhereLessThanOrEqualTo() {
-        DBObject generatedQuery = generateSimpleWhereQuery("key", Criteria.LESS_THAN_OR_EQUAL_TO, "value");
-        assertEquals(new BasicDBObject("$lte", "value"), generatedQuery.get("key"));
-    }
-
-    @Test
-    public void testMultiWhere() {
-        MongoQuery<TestClass> query = new MongoQuery<>(storage, category);
-        query.where("test", Criteria.LESS_THAN_OR_EQUAL_TO, 1);
-        query.where("test", Criteria.GREATER_THAN, 2);
-
-        DBObject generatedQuery = query.getGeneratedQuery();
-        DBObject dbObject = BasicDBObjectBuilder.start("$lte", 1).add("$gt", 2).get();
-        assertEquals(dbObject, generatedQuery.get("test"));
-    }
     
     @Test
-    public void testMultiWhere2() {
-        MongoQuery<TestClass> query = new MongoQuery<>(storage, category);
-        query.where("test", Criteria.LESS_THAN_OR_EQUAL_TO, 1);
-        query.where("test2", Criteria.GREATER_THAN, 2);
-
+    public void testWhere() {
+        MongoQuery<TestClass> query = new MongoQuery<>(storage, category, parser);
+        Expression expr = mock(Expression.class);
+        query.where(expr);
         DBObject generatedQuery = query.getGeneratedQuery();
-        assertEquals(new BasicDBObject("$lte", 1), generatedQuery.get("test"));
-    }
-    
-    @Test
-    public void testMultiWhere3() {
-        MongoQuery<TestClass> query = new MongoQuery<>(storage, category);
-        query.where("test", Criteria.EQUALS, 1);
-        query.where("test", Criteria.GREATER_THAN, 2);
-
-        DBObject generatedQuery = query.getGeneratedQuery();
-        assertEquals(new BasicDBObject("$gt", 2), generatedQuery.get("test"));
+        assertTrue(query.hasClauses());
+        assertEquals(dbObject, generatedQuery);
     }
     
-    @Test
-    public void testMultiWhere4() {
-        MongoQuery<TestClass> query = new MongoQuery<>(storage, category);
-        query.where("test", Criteria.EQUALS, 1);
-        query.where("test2", Criteria.GREATER_THAN, 2);
-
-        DBObject generatedQuery = query.getGeneratedQuery();
-        assertEquals(1, generatedQuery.get("test"));
-        assertEquals(new BasicDBObject("$gt", 2), generatedQuery.get("test2"));
-    }
-    
-    private DBObject generateSimpleWhereQuery(String key, Criteria criteria, Object value) {
-        MongoQuery<TestClass> query = new MongoQuery<>(storage, category);
-        query.where(key, criteria, value);
-        return query.getGeneratedQuery();
-    }
-
 }
 
--- a/storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoStorageTest.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoStorageTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -84,9 +84,10 @@
 import com.redhat.thermostat.storage.core.Persist;
 import com.redhat.thermostat.storage.core.Put;
 import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Update;
 import com.redhat.thermostat.storage.model.BasePojo;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 
 //There is a bug (resolved as wontfix) in powermock which results in
 //java.lang.LinkageError if javax.management.* classes aren't ignored by
@@ -223,7 +224,9 @@
     public void verifyFindAllCallsDBCollectionFind() throws Exception {
         MongoStorage storage = makeStorage();
         Query query = storage.createQuery(testCategory);
-        query.where(key1, Criteria.EQUALS, "fluff");
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.equalTo(key1, "fluff");
+        query.where(expr);
         query.execute();
         verify(testCollection).find(any(DBObject.class));
     }
--- a/thread/collector/src/main/java/com/redhat/thermostat/thread/dao/impl/ThreadDaoImpl.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/thread/collector/src/main/java/com/redhat/thermostat/thread/dao/impl/ThreadDaoImpl.java	Fri Jun 07 13:49:04 2013 -0400
@@ -44,10 +44,11 @@
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Put;
 import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 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.VmDeadLockData;
 import com.redhat.thermostat.thread.model.ThreadHarvestingStatus;
@@ -70,8 +71,11 @@
     @Override
     public VMThreadCapabilities loadCapabilities(VmRef vm) {
         Query<VMThreadCapabilities> query = storage.createQuery(THREAD_CAPABILITIES);
-        query.where(Key.VM_ID, Query.Criteria.EQUALS, vm.getId());
-        query.where(Key.AGENT_ID, Query.Criteria.EQUALS, vm.getAgent().getAgentId());
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(
+                factory.equalTo(Key.AGENT_ID, vm.getAgent().getAgentId()),
+                factory.equalTo(Key.VM_ID, vm.getId()));
+        query.where(expr);
         query.limit(1);
         Cursor<VMThreadCapabilities> cursor = query.execute();
         if (!cursor.hasNext()) {
@@ -115,9 +119,8 @@
         
         List<ThreadSummary> result = new ArrayList<>();
         
-        Query<ThreadSummary> query = prepareQuery(THREAD_SUMMARY, ref);
+        Query<ThreadSummary> query = prepareQuery(THREAD_SUMMARY, ref, since);
         query.sort(Key.TIMESTAMP, Query.SortDirection.DESCENDING);
-        query.where(Key.TIMESTAMP, Criteria.GREATER_THAN, since);
 
         Cursor<ThreadSummary> cursor = query.execute();
         while (cursor.hasNext()) {
@@ -159,8 +162,7 @@
     public List<ThreadInfoData> loadThreadInfo(VmRef ref, long since) {
         List<ThreadInfoData> result = new ArrayList<>();
         
-        Query<ThreadInfoData> query = prepareQuery(THREAD_INFO, ref);
-        query.where(Key.TIMESTAMP, Criteria.GREATER_THAN, since);
+        Query<ThreadInfoData> query = prepareQuery(THREAD_INFO, ref, since);
         query.sort(Key.TIMESTAMP, Query.SortDirection.DESCENDING);
         
         Cursor<ThreadInfoData> cursor = query.execute();
@@ -195,13 +197,21 @@
     }
     
     private <T extends Pojo> Query<T> prepareQuery(Category<T> category, VmRef vm) {
-        return prepareQuery(category, vm.getIdString(), vm.getAgent().getAgentId());
+        return prepareQuery(category, vm.getId(), vm.getAgent().getAgentId(), null);
+    }
+    
+    private <T extends Pojo> Query<T> prepareQuery(Category<T> category, VmRef vm, Long since) {
+        return prepareQuery(category, vm.getId(), vm.getAgent().getAgentId(), since);
     }
 
-    private <T extends Pojo> Query<T> prepareQuery(Category<T> category, String vmId, String agentId) {
+    private <T extends Pojo> Query<T> prepareQuery(Category<T> category, Integer vmId, String agentId, Long since) {
         Query<T> query = storage.createQuery(category);
-        query.where(Key.AGENT_ID, Query.Criteria.EQUALS, agentId);
-        query.where(Key.VM_ID, Query.Criteria.EQUALS, Integer.valueOf(vmId));
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(factory.equalTo(Key.AGENT_ID, agentId), factory.equalTo(Key.VM_ID, vmId));
+        if (since != null) {
+            expr = factory.and(expr, factory.greaterThan(Key.TIMESTAMP, since));
+        }
+        query.where(expr);
         return query;
     }
     
--- a/thread/collector/src/test/java/com/redhat/thermostat/thread/dao/impl/ThreadDaoImplTest.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/thread/collector/src/test/java/com/redhat/thermostat/thread/dao/impl/ThreadDaoImplTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -41,6 +41,7 @@
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 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.verifyNoMoreInteractions;
@@ -56,11 +57,12 @@
 import com.redhat.thermostat.storage.core.HostRef;
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Query.SortDirection;
 import com.redhat.thermostat.storage.core.Replace;
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.core.VmRef;
+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;
@@ -104,8 +106,8 @@
         ThreadDaoImpl dao = new ThreadDaoImpl(storage);
         VMThreadCapabilities caps = dao.loadCapabilities(ref);
 
-        verify(query).where(Key.VM_ID, Criteria.EQUALS, 42);
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, "0xcafe");
+        Expression expr = createWhereExpression();
+        verify(query).where(eq(expr));
         verify(query).limit(1);
         verify(query).execute();
         verifyNoMoreInteractions(query);
@@ -114,6 +116,11 @@
         assertTrue(caps.supportCPUTime());
         assertTrue(caps.supportThreadAllocatedMemory());
     }
+
+    private Expression createWhereExpression() {
+        ExpressionFactory factory = new ExpressionFactory();
+        return factory.and(factory.equalTo(Key.AGENT_ID, "0xcafe"), factory.equalTo(Key.VM_ID, 42));
+    }
     
     @Test
     public void testLoadVMCapabilitiesWithoutAnyDataInStorage() {
@@ -138,8 +145,8 @@
         ThreadDaoImpl dao = new ThreadDaoImpl(storage);
         VMThreadCapabilities caps = dao.loadCapabilities(ref);
 
-        verify(query).where(Key.VM_ID, Criteria.EQUALS, 42);
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, "0xcafe");
+        Expression expr = createWhereExpression();
+        verify(query).where(eq(expr));
         verify(query).limit(1);
         verify(query).execute();
         verifyNoMoreInteractions(query);
@@ -192,8 +199,8 @@
 
         assertSame(data, result);
 
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, agent.getAgentId());
-        verify(query).where(Key.VM_ID, Criteria.EQUALS, vm.getId());
+        Expression expr = createWhereExpression();
+        verify(query).where(eq(expr));
         verify(query).sort(Key.TIMESTAMP, SortDirection.DESCENDING);
         verify(query).execute();
         verify(query).limit(1);
@@ -239,8 +246,8 @@
         ThreadDaoImpl dao = new ThreadDaoImpl(storage);
         ThreadHarvestingStatus result = dao.getLatestHarvestingStatus(vm);
 
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, agent.getAgentId());
-        verify(query).where(Key.VM_ID, Criteria.EQUALS, vm.getId());
+        Expression expr = createWhereExpression();
+        verify(query).where(eq(expr));
         verify(query).sort(Key.TIMESTAMP, SortDirection.DESCENDING);
         verify(query).execute();
         verify(query).limit(1);
--- a/vm-classstat/common/src/test/java/com/redhat/thermostat/vm/classstat/common/internal/VmClassStatDAOTest.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/vm-classstat/common/src/test/java/com/redhat/thermostat/vm/classstat/common/internal/VmClassStatDAOTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -39,6 +39,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 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.verifyNoMoreInteractions;
@@ -51,13 +52,14 @@
 
 import com.redhat.thermostat.storage.core.Add;
 import com.redhat.thermostat.storage.core.Category;
+import com.redhat.thermostat.storage.core.Cursor;
 import com.redhat.thermostat.storage.core.HostRef;
-import com.redhat.thermostat.storage.core.VmRef;
-import com.redhat.thermostat.storage.core.Cursor;
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Storage;
+import com.redhat.thermostat.storage.core.VmRef;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 import com.redhat.thermostat.vm.classstat.common.VmClassStatDAO;
 import com.redhat.thermostat.vm.classstat.common.model.VmClassStat;
 
@@ -103,9 +105,12 @@
         VmClassStatDAO dao = new VmClassStatDAOImpl(storage);
         List<VmClassStat> vmClassStats = dao.getLatestClassStats(vmRef, Long.MIN_VALUE);
 
-        verify(query).where(Key.TIMESTAMP, Criteria.GREATER_THAN, Long.MIN_VALUE);
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, "system");
-        verify(query).where(Key.VM_ID, Criteria.EQUALS, 321);
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(
+                factory.equalTo(Key.AGENT_ID, "system"),
+                factory.and(factory.equalTo(Key.VM_ID, 321),
+                        factory.greaterThan(Key.TIMESTAMP, Long.MIN_VALUE)));
+        verify(query).where(eq(expr));
         verify(query).sort(Key.TIMESTAMP, Query.SortDirection.DESCENDING);
         verify(query).execute();
         verifyNoMoreInteractions(query);
--- a/vm-cpu/common/src/test/java/com/redhat/thermostat/vm/cpu/common/internal/VmCpuStatDAOTest.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/vm-cpu/common/src/test/java/com/redhat/thermostat/vm/cpu/common/internal/VmCpuStatDAOTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -39,6 +39,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 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.verifyNoMoreInteractions;
@@ -52,13 +53,14 @@
 
 import com.redhat.thermostat.storage.core.Add;
 import com.redhat.thermostat.storage.core.Category;
+import com.redhat.thermostat.storage.core.Cursor;
 import com.redhat.thermostat.storage.core.HostRef;
-import com.redhat.thermostat.storage.core.VmRef;
-import com.redhat.thermostat.storage.core.Cursor;
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Storage;
+import com.redhat.thermostat.storage.core.VmRef;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 import com.redhat.thermostat.vm.cpu.common.VmCpuStatDAO;
 import com.redhat.thermostat.vm.cpu.common.model.VmCpuStat;
 
@@ -111,9 +113,12 @@
         List<VmCpuStat> vmCpuStats = dao.getLatestVmCpuStats(vmRef, Long.MIN_VALUE);
 
         verify(storage).createQuery(VmCpuStatDAO.vmCpuStatCategory);
-        verify(query).where(Key.TIMESTAMP, Criteria.GREATER_THAN, Long.MIN_VALUE);
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, vmRef.getAgent().getAgentId());
-        verify(query).where(Key.VM_ID, Criteria.EQUALS, vmRef.getId());
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(
+                factory.equalTo(Key.AGENT_ID, vmRef.getAgent().getAgentId()),
+                factory.and(factory.equalTo(Key.VM_ID, vmRef.getId()),
+                        factory.greaterThan(Key.TIMESTAMP, Long.MIN_VALUE)));
+        verify(query).where(eq(expr));
         verify(query).sort(Key.TIMESTAMP, Query.SortDirection.DESCENDING);
         verify(query).execute();
         verifyNoMoreInteractions(query);
--- a/vm-gc/common/src/test/java/com/redhat/thermostat/vm/gc/common/internal/VmGcStatDAOTest.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/vm-gc/common/src/test/java/com/redhat/thermostat/vm/gc/common/internal/VmGcStatDAOTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -39,6 +39,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 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.verifyNoMoreInteractions;
@@ -51,13 +52,14 @@
 
 import com.redhat.thermostat.storage.core.Add;
 import com.redhat.thermostat.storage.core.Category;
+import com.redhat.thermostat.storage.core.Cursor;
 import com.redhat.thermostat.storage.core.HostRef;
-import com.redhat.thermostat.storage.core.VmRef;
-import com.redhat.thermostat.storage.core.Cursor;
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Storage;
+import com.redhat.thermostat.storage.core.VmRef;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 import com.redhat.thermostat.vm.gc.common.VmGcStatDAO;
 import com.redhat.thermostat.vm.gc.common.model.VmGcStat;
 
@@ -109,10 +111,12 @@
         List<VmGcStat> vmGcStats = dao.getLatestVmGcStats(vmRef, Long.MIN_VALUE);
 
         verify(storage).createQuery(VmGcStatDAO.vmGcStatCategory);
-        verify(query).where(Key.TIMESTAMP, Criteria.GREATER_THAN, Long.MIN_VALUE);
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, "system");
-        verify(query).where(Key.VM_ID, Criteria.EQUALS, 321);
-        verify(query).where(Key.TIMESTAMP, Criteria.GREATER_THAN, Long.MIN_VALUE);
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(
+                factory.equalTo(Key.AGENT_ID, vmRef.getAgent().getAgentId()),
+                factory.and(factory.equalTo(Key.VM_ID, vmRef.getId()),
+                        factory.greaterThan(Key.TIMESTAMP, Long.MIN_VALUE)));
+        verify(query).where(eq(expr));
         verify(query).sort(Key.TIMESTAMP, Query.SortDirection.DESCENDING);
         verify(query).execute();
         verifyNoMoreInteractions(query);
--- a/vm-heap-analysis/common/src/main/java/com/redhat/thermostat/vm/heap/analysis/common/internal/HeapDAOImpl.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/vm-heap-analysis/common/src/main/java/com/redhat/thermostat/vm/heap/analysis/common/internal/HeapDAOImpl.java	Fri Jun 07 13:49:04 2013 -0400
@@ -54,9 +54,10 @@
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Put;
 import com.redhat.thermostat.storage.core.Query;
+import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.core.VmRef;
-import com.redhat.thermostat.storage.core.Query.Criteria;
-import com.redhat.thermostat.storage.core.Storage;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 import com.redhat.thermostat.vm.heap.analysis.common.HeapDAO;
 import com.redhat.thermostat.vm.heap.analysis.common.HeapDump;
 import com.redhat.thermostat.vm.heap.analysis.common.ObjectHistogram;
@@ -111,8 +112,11 @@
     @Override
     public Collection<HeapInfo> getAllHeapInfo(VmRef vm) {
         Query<HeapInfo> query = storage.createQuery(heapInfoCategory);
-        query.where(Key.AGENT_ID, Criteria.EQUALS, vm.getAgent().getAgentId());
-        query.where(Key.VM_ID, Criteria.EQUALS, vm.getId());
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(
+                factory.equalTo(Key.AGENT_ID, vm.getAgent().getAgentId()),
+                factory.equalTo(Key.VM_ID, vm.getId()));
+        query.where(expr);
         Cursor<HeapInfo> cursor = query.execute();
         Collection<HeapInfo> heapInfos = new ArrayList<>();
         while (cursor.hasNext()) {
@@ -141,7 +145,9 @@
     @Override
     public HeapInfo getHeapInfo(String heapId) {
         Query<HeapInfo> query = storage.createQuery(heapInfoCategory);
-        query.where(heapIdKey, Criteria.EQUALS, heapId);
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.equalTo(heapIdKey, heapId);
+        query.where(expr);
         query.limit(1);
         HeapInfo found = null;
         try {
--- a/vm-jmx/common/src/main/java/com/redhat/thermostat/vm/jmx/common/internal/JmxNotificationDAOImpl.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/vm-jmx/common/src/main/java/com/redhat/thermostat/vm/jmx/common/internal/JmxNotificationDAOImpl.java	Fri Jun 07 13:49:04 2013 -0400
@@ -44,10 +44,11 @@
 import com.redhat.thermostat.storage.core.Cursor;
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Query.SortDirection;
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.core.VmRef;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 import com.redhat.thermostat.vm.jmx.common.JmxNotification;
 import com.redhat.thermostat.vm.jmx.common.JmxNotificationDAO;
 import com.redhat.thermostat.vm.jmx.common.JmxNotificationStatus;
@@ -90,8 +91,12 @@
     @Override
     public JmxNotificationStatus getLatestNotificationStatus(VmRef statusFor) {
         Query<JmxNotificationStatus> query = storage.createQuery(NOTIFICATION_STATUS);
-        query.where(Key.AGENT_ID, Criteria.EQUALS, statusFor.getAgent().getAgentId());
-        query.where(Key.VM_ID, Criteria.EQUALS, statusFor.getId());
+        
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(factory.equalTo(Key.AGENT_ID, statusFor
+                .getAgent().getAgentId()), factory.equalTo(Key.VM_ID,
+                statusFor.getId()));
+        query.where(expr);
 
         query.sort(Key.TIMESTAMP, SortDirection.DESCENDING);
         Cursor<JmxNotificationStatus> results = query.execute();
@@ -112,9 +117,12 @@
     @Override
     public List<JmxNotification> getNotifications(VmRef notificationsFor, long timeStampSince) {
         Query<JmxNotification> query = storage.createQuery(NOTIFICATIONS);
-        query.where(Key.AGENT_ID, Criteria.EQUALS, notificationsFor.getAgent().getAgentId());
-        query.where(Key.VM_ID, Criteria.EQUALS, notificationsFor.getId());
-        query.where(Key.TIMESTAMP, Criteria.GREATER_THAN, timeStampSince);
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(factory.equalTo(Key.AGENT_ID,
+                notificationsFor.getAgent().getAgentId()), factory.and(
+                factory.equalTo(Key.VM_ID, notificationsFor.getId()),
+                factory.greaterThan(Key.TIMESTAMP, timeStampSince)));
+        query.where(expr);
 
         List<JmxNotification> results = new ArrayList<>();
         Cursor<JmxNotification> cursor = query.execute();
--- a/vm-jmx/common/src/test/java/com/redhat/thermostat/vm/jmx/common/internal/JmxNotificationDAOImplTest.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/vm-jmx/common/src/test/java/com/redhat/thermostat/vm/jmx/common/internal/JmxNotificationDAOImplTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -39,6 +39,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -46,8 +47,6 @@
 
 import java.util.List;
 
-import javax.swing.SortOrder;
-
 import org.junit.Before;
 import org.junit.Test;
 
@@ -56,10 +55,11 @@
 import com.redhat.thermostat.storage.core.HostRef;
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Query.SortDirection;
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.core.VmRef;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 import com.redhat.thermostat.vm.jmx.common.JmxNotification;
 import com.redhat.thermostat.vm.jmx.common.JmxNotificationStatus;
 
@@ -117,8 +117,10 @@
 
         JmxNotificationStatus result = dao.getLatestNotificationStatus(vm);
 
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, AGENT_ID);
-        verify(query).where(Key.VM_ID, Criteria.EQUALS, VM_ID);
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(factory.equalTo(Key.AGENT_ID, AGENT_ID),
+                factory.equalTo(Key.VM_ID, VM_ID));
+        verify(query).where(expr);
         verify(query).sort(Key.TIMESTAMP, SortDirection.DESCENDING);
         
         assertTrue(result == data);
@@ -155,9 +157,12 @@
 
         List<JmxNotification> result = dao.getNotifications(vm, timeStamp);
 
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, AGENT_ID);
-        verify(query).where(Key.VM_ID, Criteria.EQUALS, VM_ID);
-        verify(query).where(Key.TIMESTAMP, Criteria.GREATER_THAN, timeStamp);
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(
+                factory.equalTo(Key.AGENT_ID, AGENT_ID),
+                factory.and(factory.equalTo(Key.VM_ID, VM_ID),
+                        factory.greaterThan(Key.TIMESTAMP, timeStamp)));
+        verify(query).where(eq(expr));
 
         assertEquals(1, result.size());
         assertSame(data, result.get(0));
--- a/vm-memory/common/src/main/java/com/redhat/thermostat/vm/memory/common/internal/VmMemoryStatDAOImpl.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/vm-memory/common/src/main/java/com/redhat/thermostat/vm/memory/common/internal/VmMemoryStatDAOImpl.java	Fri Jun 07 13:49:04 2013 -0400
@@ -42,10 +42,11 @@
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Put;
 import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.core.VmLatestPojoListGetter;
 import com.redhat.thermostat.storage.core.VmRef;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 import com.redhat.thermostat.vm.memory.common.VmMemoryStatDAO;
 import com.redhat.thermostat.vm.memory.common.model.VmMemoryStat;
 
@@ -63,8 +64,11 @@
     @Override
     public VmMemoryStat getLatestMemoryStat(VmRef ref) {
         Query<VmMemoryStat> query = storage.createQuery(vmMemoryStatsCategory);
-        query.where(Key.AGENT_ID, Criteria.EQUALS, ref.getAgent().getAgentId());
-        query.where(Key.VM_ID, Criteria.EQUALS, ref.getId());
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(
+                factory.equalTo(Key.AGENT_ID, ref.getAgent().getAgentId()),
+                factory.equalTo(Key.VM_ID, ref.getId()));
+        query.where(expr);
         query.sort(Key.TIMESTAMP, Query.SortDirection.DESCENDING);
         query.limit(1);
         Cursor<VmMemoryStat> cursor = query.execute();
--- a/vm-memory/common/src/test/java/com/redhat/thermostat/vm/memory/common/internal/VmMemoryStatDAOTest.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/vm-memory/common/src/test/java/com/redhat/thermostat/vm/memory/common/internal/VmMemoryStatDAOTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -39,6 +39,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 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.verifyNoMoreInteractions;
@@ -54,13 +55,14 @@
 
 import com.redhat.thermostat.storage.core.Add;
 import com.redhat.thermostat.storage.core.Category;
+import com.redhat.thermostat.storage.core.Cursor;
 import com.redhat.thermostat.storage.core.HostRef;
-import com.redhat.thermostat.storage.core.VmRef;
-import com.redhat.thermostat.storage.core.Cursor;
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Storage;
+import com.redhat.thermostat.storage.core.VmRef;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 import com.redhat.thermostat.vm.memory.common.VmMemoryStatDAO;
 import com.redhat.thermostat.vm.memory.common.model.VmMemoryStat;
 import com.redhat.thermostat.vm.memory.common.model.VmMemoryStat.Generation;
@@ -126,7 +128,11 @@
         VmMemoryStatDAO impl = new VmMemoryStatDAOImpl(storage);
         impl.getLatestMemoryStat(vmRef);
 
-        verifyQuery();
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(factory.equalTo(Key.AGENT_ID, AGENT_ID),
+                factory.equalTo(Key.VM_ID, VM_ID));
+        verify(query).where(eq(expr));
+        verify(query).sort(Key.TIMESTAMP, Query.SortDirection.DESCENDING);
     }
 
     @Test
@@ -134,20 +140,17 @@
         VmMemoryStatDAO impl = new VmMemoryStatDAOImpl(storage);
         impl.getLatestVmMemoryStats(vmRef, 123);
 
-        verifyQuery();
-
-        verify(query).where(Key.TIMESTAMP, Criteria.GREATER_THAN, 123l);
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.and(
+                factory.equalTo(Key.AGENT_ID, AGENT_ID),
+                factory.and(factory.equalTo(Key.VM_ID, VM_ID),
+                        factory.greaterThan(Key.TIMESTAMP, 123l)));
+        verify(query).where(eq(expr));
+        verify(query).sort(Key.TIMESTAMP, Query.SortDirection.DESCENDING);
         verify(query).execute();
         verifyNoMoreInteractions(query);
     }
 
-    private void verifyQuery() {
-
-        verify(query).where(Key.AGENT_ID, Criteria.EQUALS, AGENT_ID);
-        verify(query).where(Key.VM_ID, Criteria.EQUALS, VM_ID);
-        verify(query).sort(Key.TIMESTAMP, Query.SortDirection.DESCENDING);
-    }
-
     @Test
     public void testGetLatestReturnsNullWhenStorageEmpty() {
         when(cursor.hasNext()).thenReturn(false);
@@ -207,5 +210,6 @@
         verify(add).setPojo(stat);
         verify(add).apply();
     }
+    
 }
 
--- a/web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorage.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorage.java	Fri Jun 07 13:49:04 2013 -0400
@@ -103,6 +103,10 @@
 import com.redhat.thermostat.storage.core.StorageException;
 import com.redhat.thermostat.storage.core.Update;
 import com.redhat.thermostat.storage.model.Pojo;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.Operator;
+import com.redhat.thermostat.web.common.ExpressionSerializer;
+import com.redhat.thermostat.web.common.OperatorSerializer;
 import com.redhat.thermostat.web.common.ThermostatGSONConverter;
 import com.redhat.thermostat.web.common.WebInsert;
 import com.redhat.thermostat.web.common.WebQuery;
@@ -347,7 +351,10 @@
     private void init(StartupConfiguration config, DefaultHttpClient client, ClientConnectionManager connManager) {
         categoryIds = new HashMap<>();
         gson = new GsonBuilder().registerTypeHierarchyAdapter(Pojo.class,
-                new ThermostatGSONConverter()).create();
+                        new ThermostatGSONConverter())
+                .registerTypeHierarchyAdapter(Expression.class,
+                        new ExpressionSerializer())
+                .registerTypeHierarchyAdapter(Operator.class, new OperatorSerializer()).create();
         httpClient = client;
         random = new SecureRandom();
         conn = new WebConnection();
--- a/web/client/src/test/java/com/redhat/thermostat/web/client/internal/WebStorageTest.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/web/client/src/test/java/com/redhat/thermostat/web/client/internal/WebStorageTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -85,6 +85,7 @@
 import org.mockito.Mockito;
 
 import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonParser;
 import com.google.gson.JsonSyntaxException;
@@ -101,8 +102,13 @@
 import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Remove;
 import com.redhat.thermostat.storage.core.Update;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
+import com.redhat.thermostat.storage.query.Operator;
 import com.redhat.thermostat.test.FreePortFinder;
 import com.redhat.thermostat.test.FreePortFinder.TryPort;
+import com.redhat.thermostat.web.common.ExpressionSerializer;
+import com.redhat.thermostat.web.common.OperatorSerializer;
 import com.redhat.thermostat.web.common.Qualifier;
 import com.redhat.thermostat.web.common.WebInsert;
 import com.redhat.thermostat.web.common.WebQuery;
@@ -237,12 +243,15 @@
         obj1.setProperty1("fluffor1");
         TestObj obj2 = new TestObj();
         obj2.setProperty1("fluffor2");
-        Gson gson = new Gson();
+        Gson gson = new GsonBuilder().registerTypeHierarchyAdapter(Expression.class, new ExpressionSerializer())
+                .registerTypeHierarchyAdapter(Operator.class, new OperatorSerializer()).create();
         responseBody = gson.toJson(Arrays.asList(obj1, obj2));
 
         Key<String> key1 = new Key<>("property1", true);
         Query<TestObj> query = storage.createQuery(category);
-        query.where(key1, Criteria.EQUALS, "fluff");
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.equalTo(key1, "fluff");
+        query.where(expr);
 
         Cursor<TestObj> results = query.execute();
         StringReader reader = new StringReader(requestBody);
@@ -253,12 +262,8 @@
         WebQuery<?> restQuery = gson.fromJson(parts[1], WebQuery.class);
 
         assertEquals(42, restQuery.getCategoryId());
-        List<Qualifier<?>> qualifiers = restQuery.getQualifiers();
-        assertEquals(1, qualifiers.size());
-        Qualifier<?> qual = qualifiers.get(0);
-        assertEquals(new Key<String>("property1", true), qual.getKey());
-        assertEquals(Criteria.EQUALS, qual.getCriteria());
-        assertEquals("fluff", qual.getValue());
+        Expression restExpr = restQuery.getExpression();
+        assertEquals(expr, restExpr);
 
         assertTrue(results.hasNext());
         assertEquals("fluffor1", results.next().getProperty1());
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/common/src/main/java/com/redhat/thermostat/web/common/ExpressionSerializer.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,207 @@
+/*
+ * 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 java.lang.reflect.Type;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.redhat.thermostat.storage.core.Key;
+import com.redhat.thermostat.storage.query.BinaryComparisonExpression;
+import com.redhat.thermostat.storage.query.BinaryComparisonOperator;
+import com.redhat.thermostat.storage.query.BinaryLogicalExpression;
+import com.redhat.thermostat.storage.query.BinaryLogicalOperator;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.LiteralExpression;
+import com.redhat.thermostat.storage.query.Operator;
+import com.redhat.thermostat.storage.query.UnaryLogicalExpression;
+import com.redhat.thermostat.storage.query.UnaryLogicalOperator;
+
+public class ExpressionSerializer implements JsonSerializer<Expression>,
+        JsonDeserializer<Expression> {
+    /* The concrete Expression fully-qualified class name */
+    static final String PROP_CLASS_NAME = "PROP_CLASS_NAME";
+    /* Serialized operand for a UnaryExpression */
+    static final String PROP_OPERAND = "PROP_OPERAND";
+    /* Serialized left operand for a BinaryExpression */
+    static final String PROP_OPERAND_LEFT = "PROP_OPERAND_LEFT";
+    /* Serialized right operand for a BinaryExpression */
+    static final String PROP_OPERAND_RIGHT = "PROP_OPERAND_RIGHT";
+    /* Serialized operator for an Expression */
+    static final String PROP_OPERATOR = "PROP_OPERATOR";
+    /* Serialized value for a LiteralExpression */
+    static final String PROP_VALUE = "PROP_VALUE";
+    /* Fully-qualified class name of a LiteralExpression's value */
+    static final String PROP_VALUE_CLASS = "PROP_VALUE_CLASS";
+    
+    @Override
+    public Expression deserialize(JsonElement json, Type typeOfT,
+            JsonDeserializationContext context) throws JsonParseException {
+        JsonElement jsonClassName = json.getAsJsonObject().get(PROP_CLASS_NAME);
+        if (jsonClassName == null) {
+            throw new JsonParseException("No class name property provided");
+        }
+        String className = jsonClassName.getAsString();
+        Expression result;
+        try {
+            Class<?> clazz = (Class<?>) Class.forName(className);
+            // Deserialize using concrete implementations to avoid reflection
+            // and unchecked casts
+            if (BinaryComparisonExpression.class.isAssignableFrom(clazz)) {
+                result = deserializeBinaryComparisonExpression(json, context);
+            }
+            else if (BinaryLogicalExpression.class.isAssignableFrom(clazz)) {
+                result = deserializeBinaryLogicalExpression(json, context);
+            }
+            else if (UnaryLogicalExpression.class.isAssignableFrom(clazz)) {
+                result = deserializeUnaryLogicalExpression(json, context);
+            }
+            else if (LiteralExpression.class.isAssignableFrom(clazz)) {
+                result = deserializeLiteralExpression(json, context);
+            }
+            else {
+                throw new JsonParseException("Unknown Expression of type " + className);
+            }
+        } catch (ClassNotFoundException e) {
+            throw new JsonParseException("Unable to deserialize Expression", e);
+        }
+        return result;
+    }
+
+    private <T> Expression deserializeBinaryComparisonExpression(JsonElement json,
+            JsonDeserializationContext context) {
+        JsonElement jsonLeft = json.getAsJsonObject().get(PROP_OPERAND_LEFT);
+        JsonElement jsonRight = json.getAsJsonObject().get(PROP_OPERAND_RIGHT);
+        JsonElement jsonOp = json.getAsJsonObject().get(PROP_OPERATOR);
+        
+        LiteralExpression<Key<T>> left = context.deserialize(jsonLeft, Expression.class);
+        LiteralExpression<T> right = context.deserialize(jsonRight, Expression.class);
+        BinaryComparisonOperator op = context.deserialize(jsonOp, Operator.class);
+        return new BinaryComparisonExpression<>(left, op, right);
+    }
+
+    private <S extends Expression, T extends Expression> Expression deserializeBinaryLogicalExpression(JsonElement json,
+            JsonDeserializationContext context) {
+        JsonElement jsonLeft = json.getAsJsonObject().get(PROP_OPERAND_LEFT);
+        JsonElement jsonRight = json.getAsJsonObject().get(PROP_OPERAND_RIGHT);
+        JsonElement jsonOp = json.getAsJsonObject().get(PROP_OPERATOR);
+        
+        S left = context.deserialize(jsonLeft, Expression.class);
+        T right = context.deserialize(jsonRight, Expression.class);
+        BinaryLogicalOperator op = context.deserialize(jsonOp, Operator.class);
+        return new BinaryLogicalExpression<>(left, op, right);
+    }
+
+    private <T extends Expression> Expression deserializeUnaryLogicalExpression(JsonElement json,
+            JsonDeserializationContext context) {
+        JsonElement jsonOperand = json.getAsJsonObject().get(PROP_OPERAND);
+        JsonElement jsonOp = json.getAsJsonObject().get(PROP_OPERATOR);
+        
+        T operand = context.deserialize(jsonOperand, Expression.class);
+        UnaryLogicalOperator operator = context.deserialize(jsonOp, Operator.class);
+        return new UnaryLogicalExpression<>(operand, operator);
+    }
+
+    private Expression deserializeLiteralExpression(JsonElement json,
+            JsonDeserializationContext context) throws ClassNotFoundException {
+        JsonElement jsonValue = json.getAsJsonObject().get(PROP_VALUE);
+        JsonElement jsonValueClass = json.getAsJsonObject().get(PROP_VALUE_CLASS);
+        String valueClassName = jsonValueClass.getAsString();
+        Class<?> valueClass = Class.forName(valueClassName);
+        return makeLiteralExpression(context, jsonValue, valueClass);
+    }
+
+    private <T> Expression makeLiteralExpression(JsonDeserializationContext context, 
+            JsonElement jsonValue, Class<T> valueClass) throws ClassNotFoundException {
+        T value = context.deserialize(jsonValue, valueClass);
+        return new LiteralExpression<>(value);
+    }
+
+    @Override
+    public JsonElement serialize(Expression src, Type typeOfSrc,
+            JsonSerializationContext context) {
+        JsonObject result;
+        // Only concrete implementations are public
+        if (src instanceof BinaryLogicalExpression) {
+            BinaryLogicalExpression<?, ?> binExpr = (BinaryLogicalExpression<?, ?>) src;
+            JsonElement left = context.serialize(binExpr.getLeftOperand());
+            JsonElement op = context.serialize(binExpr.getOperator());
+            JsonElement right = context.serialize(binExpr.getRightOperand());
+            result = new JsonObject();
+            result.add(PROP_OPERAND_LEFT, left);
+            result.add(PROP_OPERATOR, op);
+            result.add(PROP_OPERAND_RIGHT, right);
+        }
+        else if (src instanceof BinaryComparisonExpression) {
+            BinaryComparisonExpression<?> binExpr = (BinaryComparisonExpression<?>) src;
+            JsonElement left = context.serialize(binExpr.getLeftOperand());
+            JsonElement op = context.serialize(binExpr.getOperator());
+            JsonElement right = context.serialize(binExpr.getRightOperand());
+            result = new JsonObject();
+            result.add(PROP_OPERAND_LEFT, left);
+            result.add(PROP_OPERATOR, op);
+            result.add(PROP_OPERAND_RIGHT, right);
+        }
+        else if (src instanceof UnaryLogicalExpression) {
+            UnaryLogicalExpression<?> unaryExpr = (UnaryLogicalExpression<?>) src;
+            JsonElement operand = context.serialize(unaryExpr.getOperand());
+            JsonElement operator = context.serialize(unaryExpr.getOperator());
+            result = new JsonObject();
+            result.add(PROP_OPERAND, operand);
+            result.add(PROP_OPERATOR, operator);
+        }
+        else if (src instanceof LiteralExpression) {
+            LiteralExpression<?> litExpr = (LiteralExpression<?>) src;
+            JsonElement value = context.serialize(litExpr.getValue());
+            result = new JsonObject();
+            result.add(PROP_VALUE, value);
+            // Store the type of value to properly deserialize it later
+            result.addProperty(PROP_VALUE_CLASS, litExpr.getValue().getClass().getCanonicalName());
+        }
+        else {
+            throw new JsonParseException("Unknown expression of type " + src.getClass());
+        }
+        result.addProperty(PROP_CLASS_NAME, src.getClass().getCanonicalName());
+        return result;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/common/src/main/java/com/redhat/thermostat/web/common/OperatorSerializer.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,104 @@
+/*
+ * 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 java.lang.reflect.Type;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.redhat.thermostat.storage.query.Operator;
+
+public class OperatorSerializer implements JsonDeserializer<Operator>,
+        JsonSerializer<Operator> {
+    /* The name of the enum constant */
+    static final String PROP_CONST = "PROP_CONST";
+    /* The Operator implementation fully-qualified class name */
+    static final String PROP_CLASS_NAME = "PROP_CLASS_NAME";
+
+    @Override
+    public JsonElement serialize(Operator src, Type typeOfSrc,
+            JsonSerializationContext context) {
+        JsonObject result = new JsonObject();
+        // All concrete Operators should be Enums
+        if (src instanceof Enum) {
+            Enum<?> operator = (Enum<?>) src;
+            result.addProperty(PROP_CONST, operator.name());
+            result.addProperty(PROP_CLASS_NAME, operator.getDeclaringClass().getCanonicalName());
+        }
+        else {
+            throw new JsonParseException("Concrete Operator must be an enum");
+        }
+        return result;
+    }
+
+    @Override
+    public Operator deserialize(JsonElement json, Type typeOfT,
+            JsonDeserializationContext context) throws JsonParseException {
+        JsonElement jsonClassName = json.getAsJsonObject().get(PROP_CLASS_NAME);
+        if (jsonClassName == null) {
+            throw new JsonParseException("Class name must be specified for Operator");
+        }
+        String className = jsonClassName.getAsString();
+        Operator result;
+        try {
+            Class<?> clazz = Class.forName(className);
+            result = getEnum(json, clazz);
+        } catch (ClassNotFoundException e) {
+            throw new JsonParseException("Unable to deserialize Operator", e);
+        }
+        return result;
+    }
+
+    private <T extends Enum<T> & Operator> Operator getEnum(JsonElement json, Class<?> clazz) {
+        if (Operator.class.isAssignableFrom(clazz) && clazz.isEnum()) {
+            @SuppressWarnings("unchecked") // Checked with above condition
+            Class<T> operatorClass = (Class<T>) clazz;
+            JsonElement jsonConst = json.getAsJsonObject().get(PROP_CONST);
+            String enumConst = jsonConst.getAsString();
+            return Enum.valueOf(operatorClass, enumConst);
+        }
+        else {
+            throw new JsonParseException(clazz.getName() + " must be an Enum implementing Operator");
+        }
+    }
+
+}
--- a/web/common/src/main/java/com/redhat/thermostat/web/common/WebQuery.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/web/common/src/main/java/com/redhat/thermostat/web/common/WebQuery.java	Fri Jun 07 13:49:04 2013 -0400
@@ -37,18 +37,14 @@
 
 package com.redhat.thermostat.web.common;
 
-import java.util.ArrayList;
-import java.util.List;
-
 import com.redhat.thermostat.storage.core.AbstractQuery;
 import com.redhat.thermostat.storage.core.Cursor;
-import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.model.Pojo;
+import com.redhat.thermostat.storage.query.Expression;
 
 public class WebQuery<T extends Pojo> extends AbstractQuery<T> {
 
-    private List<Qualifier<?>> qualifiers;
-
+    private Expression expr;
     private int categoryId;
 
     public WebQuery() {
@@ -56,7 +52,6 @@
     }
 
     public WebQuery(int categoryId) {
-        qualifiers = new ArrayList<>();
         this.categoryId = categoryId;
     }
 
@@ -69,16 +64,12 @@
     }
 
     @Override
-    public <S> void where(Key<S> key, Criteria criteria, S value) {
-        qualifiers.add(new Qualifier<>(key, criteria, value));
+    public void where(Expression expr) {
+        this.expr = expr;
     }
-
-    public List<Qualifier<?>> getQualifiers() {
-        return qualifiers;
-    }
-
-    public void setQualifiers(List<Qualifier<?>> qualifiers) {
-        this.qualifiers = qualifiers;
+    
+    public Expression getExpression() {
+        return expr;
     }
 
     @Override
--- a/web/common/src/main/java/com/redhat/thermostat/web/common/WebRemove.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/web/common/src/main/java/com/redhat/thermostat/web/common/WebRemove.java	Fri Jun 07 13:49:04 2013 -0400
@@ -42,8 +42,8 @@
 
 import com.redhat.thermostat.storage.core.Category;
 import com.redhat.thermostat.storage.core.Key;
+import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Remove;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 
 public class WebRemove implements Remove {
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/common/src/test/java/com/redhat/thermostat/web/common/ExpressionSerializerTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,201 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.redhat.thermostat.storage.core.Key;
+import com.redhat.thermostat.storage.query.BinaryComparisonExpression;
+import com.redhat.thermostat.storage.query.BinaryLogicalExpression;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
+import com.redhat.thermostat.storage.query.LiteralExpression;
+import com.redhat.thermostat.storage.query.Operator;
+import com.redhat.thermostat.storage.query.UnaryLogicalExpression;
+
+public class ExpressionSerializerTest {
+
+    private static final Key<String> key = new Key<>("hello", true);
+    private Gson gson;
+
+    private static final class TestExpression implements Expression {
+        
+    }
+    
+    @Before
+    public void setUp() throws Exception {
+        gson = new GsonBuilder()
+                .registerTypeHierarchyAdapter(Expression.class,
+                        new ExpressionSerializer())
+                .registerTypeHierarchyAdapter(Operator.class,
+                        new OperatorSerializer()).create();
+    }
+
+    @Test
+    public void testDeserializeLiteralExpression() {
+        Expression expr = new LiteralExpression<String>("hello");
+        
+        JsonObject json = new JsonObject();
+        json.addProperty(ExpressionSerializer.PROP_VALUE, "hello");
+        json.addProperty(ExpressionSerializer.PROP_VALUE_CLASS, String.class.getCanonicalName());
+        json.addProperty(ExpressionSerializer.PROP_CLASS_NAME, LiteralExpression.class.getCanonicalName());
+        
+        assertEquals(expr, gson.fromJson(json, Expression.class));
+    }
+    
+    @Test
+    public void testDeserializeBinaryComparisonExpression() {
+        ExpressionFactory factory = new ExpressionFactory();
+        BinaryComparisonExpression<String> expr = factory.equalTo(key, "world");
+        
+        JsonObject json = new JsonObject();
+        json.add(ExpressionSerializer.PROP_OPERAND_LEFT, gson.toJsonTree(expr.getLeftOperand()));
+        json.add(ExpressionSerializer.PROP_OPERATOR, gson.toJsonTree(expr.getOperator()));
+        json.add(ExpressionSerializer.PROP_OPERAND_RIGHT, gson.toJsonTree(expr.getRightOperand()));
+        json.addProperty(ExpressionSerializer.PROP_CLASS_NAME, BinaryComparisonExpression.class.getCanonicalName());
+        
+        assertEquals(expr, gson.fromJson(json, Expression.class));
+    }
+    
+    @Test
+    public void testDeserializeBinaryLogicalExpression() {
+        ExpressionFactory factory = new ExpressionFactory();
+        BinaryLogicalExpression<BinaryComparisonExpression<String>, BinaryComparisonExpression<String>> expr = factory.and(factory.equalTo(key, "world"), factory.greaterThan(key, "goodbye"));
+        
+        JsonObject json = new JsonObject();
+        json.add(ExpressionSerializer.PROP_OPERAND_LEFT, gson.toJsonTree(expr.getLeftOperand()));
+        json.add(ExpressionSerializer.PROP_OPERATOR, gson.toJsonTree(expr.getOperator()));
+        json.add(ExpressionSerializer.PROP_OPERAND_RIGHT, gson.toJsonTree(expr.getRightOperand()));
+        json.addProperty(ExpressionSerializer.PROP_CLASS_NAME, BinaryLogicalExpression.class.getCanonicalName());
+        
+        assertEquals(expr, gson.fromJson(json, Expression.class));
+    }
+    
+    @Test
+    public void testDeserializeUnaryLogicalExpression() {
+        ExpressionFactory factory = new ExpressionFactory();
+        UnaryLogicalExpression<BinaryComparisonExpression<String>> expr = factory.not(factory.greaterThan(key, "goodbye"));
+        
+        JsonObject json = new JsonObject();
+        json.add(ExpressionSerializer.PROP_OPERAND, gson.toJsonTree(expr.getOperand()));
+        json.add(ExpressionSerializer.PROP_OPERATOR, gson.toJsonTree(expr.getOperator()));
+        json.addProperty(ExpressionSerializer.PROP_CLASS_NAME, UnaryLogicalExpression.class.getCanonicalName());
+        
+        assertEquals(expr, gson.fromJson(json, Expression.class));
+    }
+    
+    @Test(expected=JsonParseException.class)
+    public void testDeserializeNoClassName() {
+        JsonObject json = new JsonObject();
+        
+        gson.fromJson(json, Expression.class);
+    }
+    
+    @Test(expected=JsonParseException.class)
+    public void testDeserializeUnknownExpression() {
+        JsonObject json = new JsonObject();
+        json.addProperty(ExpressionSerializer.PROP_CLASS_NAME, TestExpression.class.getCanonicalName());
+        
+        gson.fromJson(json, Expression.class);
+    }
+
+    @Test
+    public void testSerializeLiteralExpression() {
+        Expression expr = new LiteralExpression<String>("hello");
+        
+        JsonObject json = new JsonObject();
+        json.addProperty(ExpressionSerializer.PROP_VALUE, "hello");
+        json.addProperty(ExpressionSerializer.PROP_VALUE_CLASS, String.class.getCanonicalName());
+        json.addProperty(ExpressionSerializer.PROP_CLASS_NAME, LiteralExpression.class.getCanonicalName());
+        
+        assertEquals(gson.toJson(json), gson.toJson(expr));
+    }
+    
+    @Test
+    public void testSerializeBinaryComparisonExpression() {
+        ExpressionFactory factory = new ExpressionFactory();
+        BinaryComparisonExpression<String> expr = factory.equalTo(key, "world");
+        
+        JsonObject json = new JsonObject();
+        json.add(ExpressionSerializer.PROP_OPERAND_LEFT, gson.toJsonTree(expr.getLeftOperand()));
+        json.add(ExpressionSerializer.PROP_OPERATOR, gson.toJsonTree(expr.getOperator()));
+        json.add(ExpressionSerializer.PROP_OPERAND_RIGHT, gson.toJsonTree(expr.getRightOperand()));
+        json.addProperty(ExpressionSerializer.PROP_CLASS_NAME, BinaryComparisonExpression.class.getCanonicalName());
+        
+        assertEquals(gson.toJson(json), gson.toJson(expr));
+    }
+    
+    @Test
+    public void testSerializeBinaryLogicalExpression() {
+        ExpressionFactory factory = new ExpressionFactory();
+        BinaryLogicalExpression<BinaryComparisonExpression<String>, BinaryComparisonExpression<String>> expr = factory.and(factory.equalTo(key, "world"), factory.greaterThan(key, "goodbye"));
+        
+        JsonObject json = new JsonObject();
+        json.add(ExpressionSerializer.PROP_OPERAND_LEFT, gson.toJsonTree(expr.getLeftOperand()));
+        json.add(ExpressionSerializer.PROP_OPERATOR, gson.toJsonTree(expr.getOperator()));
+        json.add(ExpressionSerializer.PROP_OPERAND_RIGHT, gson.toJsonTree(expr.getRightOperand()));
+        json.addProperty(ExpressionSerializer.PROP_CLASS_NAME, BinaryLogicalExpression.class.getCanonicalName());
+        
+        assertEquals(gson.toJson(json), gson.toJson(expr));
+    }
+    
+    @Test
+    public void testSerializeUnaryLogicalExpression() {
+        ExpressionFactory factory = new ExpressionFactory();
+        UnaryLogicalExpression<BinaryComparisonExpression<String>> expr = factory.not(factory.greaterThan(key, "goodbye"));
+        
+        JsonObject json = new JsonObject();
+        json.add(ExpressionSerializer.PROP_OPERAND, gson.toJsonTree(expr.getOperand()));
+        json.add(ExpressionSerializer.PROP_OPERATOR, gson.toJsonTree(expr.getOperator()));
+        json.addProperty(ExpressionSerializer.PROP_CLASS_NAME, UnaryLogicalExpression.class.getCanonicalName());
+        
+        assertEquals(gson.toJson(json), gson.toJson(expr));
+    }
+    
+    @Test(expected=JsonParseException.class)
+    public void testSerializeUnknownExpression() {
+        gson.toJson(new TestExpression());
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/common/src/test/java/com/redhat/thermostat/web/common/OperatorSerializerTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -0,0 +1,154 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.redhat.thermostat.storage.query.BinaryComparisonOperator;
+import com.redhat.thermostat.storage.query.BinaryLogicalOperator;
+import com.redhat.thermostat.storage.query.Operator;
+import com.redhat.thermostat.storage.query.UnaryLogicalOperator;
+
+public class OperatorSerializerTest {
+
+    private Gson gson;
+
+    private static final class TestOperator implements Operator {
+        
+    }
+    
+    @Before
+    public void setUp() throws Exception {
+        gson = new GsonBuilder()
+                .registerTypeHierarchyAdapter(Operator.class,
+                        new OperatorSerializer()).create();
+    }
+
+    @Test
+    public void testDeserializeBinaryComparisonOperator() {
+        Operator op = BinaryComparisonOperator.EQUALS;
+        
+        JsonObject json = new JsonObject();
+        json.addProperty(OperatorSerializer.PROP_CONST, BinaryComparisonOperator.EQUALS.name());
+        json.addProperty(OperatorSerializer.PROP_CLASS_NAME, BinaryComparisonOperator.class.getCanonicalName());
+        
+        assertEquals(op, gson.fromJson(json, Operator.class));
+    }
+    
+    @Test
+    public void testDeserializeBinaryLogicalOperator() {
+        Operator op = BinaryLogicalOperator.AND;
+        
+        JsonObject json = new JsonObject();
+        json.addProperty(OperatorSerializer.PROP_CONST, BinaryLogicalOperator.AND.name());
+        json.addProperty(OperatorSerializer.PROP_CLASS_NAME, BinaryLogicalOperator.class.getCanonicalName());
+        
+        assertEquals(op, gson.fromJson(json, Operator.class));
+    }
+    
+    @Test
+    public void testDeserializeUnaryLogicalOperator() {
+        Operator op = UnaryLogicalOperator.NOT;
+        
+        JsonObject json = new JsonObject();
+        json.addProperty(OperatorSerializer.PROP_CONST, UnaryLogicalOperator.NOT.name());
+        json.addProperty(OperatorSerializer.PROP_CLASS_NAME, UnaryLogicalOperator.class.getCanonicalName());
+        
+        assertEquals(op, gson.fromJson(json, Operator.class));
+    }
+    
+    @Test(expected=JsonParseException.class)
+    public void testDeserializeNoClassName() {
+        JsonObject json = new JsonObject();
+        
+        gson.fromJson(json, Operator.class);
+    }
+    
+    @Test(expected=JsonParseException.class)
+    public void testDeserializeUnknownOperator() {
+        JsonObject json = new JsonObject();
+        json.addProperty(OperatorSerializer.PROP_CLASS_NAME, TestOperator.class.getCanonicalName());
+        
+        gson.fromJson(json, Operator.class);
+    }
+
+    @Test
+    public void testSerializeBinaryComparisonOperator() {
+        Operator op = BinaryComparisonOperator.EQUALS;
+        
+        JsonObject json = new JsonObject();
+        json.addProperty(OperatorSerializer.PROP_CONST, BinaryComparisonOperator.EQUALS.name());
+        json.addProperty(OperatorSerializer.PROP_CLASS_NAME, BinaryComparisonOperator.class.getCanonicalName());
+        
+        assertEquals(gson.toJson(json), gson.toJson(op));
+    }
+    
+    @Test
+    public void testSerializeBinaryLogicalOperator() {
+        Operator op = BinaryLogicalOperator.AND;
+        
+        JsonObject json = new JsonObject();
+        json.addProperty(OperatorSerializer.PROP_CONST, BinaryLogicalOperator.AND.name());
+        json.addProperty(OperatorSerializer.PROP_CLASS_NAME, BinaryLogicalOperator.class.getCanonicalName());
+        
+        assertEquals(gson.toJson(json), gson.toJson(op));
+    }
+    
+    @Test
+    public void testSerializeUnaryLogicalOperator() {
+        Operator op = UnaryLogicalOperator.NOT;
+        
+        JsonObject json = new JsonObject();
+        json.addProperty(OperatorSerializer.PROP_CONST, UnaryLogicalOperator.NOT.name());
+        json.addProperty(OperatorSerializer.PROP_CLASS_NAME, UnaryLogicalOperator.class.getCanonicalName());
+        
+        assertEquals(gson.toJson(json), gson.toJson(op));
+    }
+    
+    @Test(expected=JsonParseException.class)
+    public void testSerializeUnknownOperator() {
+        gson.toJson(new TestOperator());
+    }
+
+}
--- a/web/common/src/test/java/com/redhat/thermostat/web/common/WebQueryTest.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/web/common/src/test/java/com/redhat/thermostat/web/common/WebQueryTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -35,18 +35,20 @@
  */
 
 package com.redhat.thermostat.web.common;
+
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 
 import org.junit.Test;
 
 import com.redhat.thermostat.storage.core.Category;
 import com.redhat.thermostat.storage.core.Key;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.model.Pojo;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
 
 public class WebQueryTest {
 
@@ -61,14 +63,13 @@
         Map<Category,Integer> categoryIdMap = new HashMap<>();
         categoryIdMap.put(category, 42);
         WebQuery query = new WebQuery(42);
-        query.where(key1, Criteria.EQUALS, "fluff");
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.equalTo(key1, "fluff");
+        query.where(expr);
 
-        List<Qualifier<?>> qualifiers = query.getQualifiers();
-        assertEquals(1, qualifiers.size());
-        Qualifier<?> qualifier = qualifiers.get(0);
-        assertEquals(key1, qualifier.getKey());
-        assertEquals(Criteria.EQUALS, qualifier.getCriteria());
-        assertEquals("fluff", qualifier.getValue());
+        Expression retExpr = query.getExpression();
+        assertNotNull(retExpr);
+        assertEquals(expr, retExpr);
 
         assertEquals(42, query.getCategoryId());
     }
--- a/web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java	Fri Jun 07 13:49:04 2013 -0400
@@ -80,6 +80,10 @@
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.core.Update;
 import com.redhat.thermostat.storage.model.Pojo;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.Operator;
+import com.redhat.thermostat.web.common.ExpressionSerializer;
+import com.redhat.thermostat.web.common.OperatorSerializer;
 import com.redhat.thermostat.web.common.Qualifier;
 import com.redhat.thermostat.web.common.StorageWrapper;
 import com.redhat.thermostat.web.common.ThermostatGSONConverter;
@@ -124,7 +128,13 @@
         // check if thermostat home is set and readable
         checkThermostatHome();
         
-        gson = new GsonBuilder().registerTypeHierarchyAdapter(Pojo.class, new ThermostatGSONConverter()).create();
+        gson = new GsonBuilder()
+                .registerTypeHierarchyAdapter(Pojo.class,
+                        new ThermostatGSONConverter())
+                .registerTypeHierarchyAdapter(Expression.class,
+                        new ExpressionSerializer())
+                .registerTypeHierarchyAdapter(Operator.class,
+                        new OperatorSerializer()).create();
         categoryIds = new HashMap<>();
         categories = new HashMap<>();
         TokenManager tokenManager = new TokenManager();
@@ -456,15 +466,14 @@
         writeResponse(resp, resultList.toArray());
     }
 
-    @SuppressWarnings({ "rawtypes", "unchecked" })
     private Query<?> constructTargetQuery(WebQuery<? extends Pojo> query) {
         int categoryId = query.getCategoryId();
         Category<?> category = getCategoryFromId(categoryId);
 
         Query<?> targetQuery = storage.createQuery(category);
-        List<Qualifier<?>> qualifiers = query.getQualifiers();
-        for (Qualifier q : qualifiers) {
-            targetQuery.where(q.getKey(), q.getCriteria(), q.getValue());
+        Expression whereExpr = query.getExpression();
+        if (whereExpr != null) {
+            targetQuery.where(whereExpr);
         }
         for (Sort s : query.getSorts()) {
             targetQuery.sort(s.getKey(), s.getDirection());
--- a/web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java	Fri Jun 07 11:38:33 2013 -0400
+++ b/web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java	Fri Jun 07 13:49:04 2013 -0400
@@ -84,6 +84,7 @@
 import org.mockito.ArgumentCaptor;
 
 import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
 import com.redhat.thermostat.storage.core.Add;
 import com.redhat.thermostat.storage.core.Categories;
 import com.redhat.thermostat.storage.core.Category;
@@ -92,15 +93,19 @@
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Persist;
 import com.redhat.thermostat.storage.core.Query;
-import com.redhat.thermostat.storage.core.Query.Criteria;
 import com.redhat.thermostat.storage.core.Query.SortDirection;
 import com.redhat.thermostat.storage.core.Remove;
 import com.redhat.thermostat.storage.core.Replace;
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.core.Update;
 import com.redhat.thermostat.storage.model.BasePojo;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.ExpressionFactory;
+import com.redhat.thermostat.storage.query.Operator;
 import com.redhat.thermostat.test.FreePortFinder;
 import com.redhat.thermostat.test.FreePortFinder.TryPort;
+import com.redhat.thermostat.web.common.ExpressionSerializer;
+import com.redhat.thermostat.web.common.OperatorSerializer;
 import com.redhat.thermostat.web.common.StorageWrapper;
 import com.redhat.thermostat.web.common.WebInsert;
 import com.redhat.thermostat.web.common.WebQuery;
@@ -298,10 +303,16 @@
         Map<Category,Integer> categoryIdMap = new HashMap<>();
         categoryIdMap.put(category, categoryId);
         WebQuery query = new WebQuery(categoryId);
-        query.where(key1, Criteria.EQUALS, "fluff");
+        ExpressionFactory factory = new ExpressionFactory();
+        Expression expr = factory.equalTo(key1, "fluff");
+        query.where(expr);
         query.sort(key1, SortDirection.DESCENDING);
         query.limit(42);
-        Gson gson = new Gson();
+        Gson gson = new GsonBuilder()
+                .registerTypeHierarchyAdapter(Expression.class,
+                        new ExpressionSerializer())
+                .registerTypeHierarchyAdapter(Operator.class,
+                        new OperatorSerializer()).create();
         OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
         String body = "query=" + URLEncoder.encode(gson.toJson(query), "UTF-8");
         out.write(body + "\n");
@@ -317,7 +328,7 @@
 
         assertEquals("application/json; charset=UTF-8", conn.getContentType());
 
-        verify(mockQuery).where(key1, Criteria.EQUALS, "fluff");
+        verify(mockQuery).where(eq(expr));
         verify(mockQuery).sort(key1, SortDirection.DESCENDING);
         verify(mockQuery).limit(42);
         verify(mockQuery).execute();