changeset 1148:e597d72c3ecb

Add support for IN / NOT IN operators This commit extends our query mechanism to handle queries of the form: key IN [ x, y, z ], and key NOT IN [ x, y, z ]. I have added two new Expression implementations, a BinarySetMembershipExpression and a LiteralSetExpression. LiteralSetExpression corresponds to a list of values, which can then be used as the right operand to a BinarySetMembershipExpression. We have to explicitly pass the type of value we're using in our list for deserialization. I have also cleaned up the MongoExpressionParser in the process of adding support for these new operators. UnaryLogicalExpressions now can only take ComparisonExpressions, due to the restriction of how $not is used in MongoDB. Reviewed-by: vanaltj Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-June/007100.html
author Elliott Baron <ebaron@redhat.com>
date Fri, 28 Jun 2013 12:37:39 -0400
parents 52736b6a71d2
children d1f8a091dd68
files storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryComparisonExpression.java storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryLogicalExpression.java storage/core/src/main/java/com/redhat/thermostat/storage/query/BinarySetMembershipExpression.java storage/core/src/main/java/com/redhat/thermostat/storage/query/BinarySetMembershipOperator.java storage/core/src/main/java/com/redhat/thermostat/storage/query/ComparisonExpression.java storage/core/src/main/java/com/redhat/thermostat/storage/query/ExpressionFactory.java storage/core/src/main/java/com/redhat/thermostat/storage/query/LiteralSetExpression.java storage/core/src/main/java/com/redhat/thermostat/storage/query/LogicalExpression.java storage/core/src/main/java/com/redhat/thermostat/storage/query/UnaryLogicalExpression.java storage/core/src/test/java/com/redhat/thermostat/storage/query/ExpressionFactoryTest.java storage/core/src/test/java/com/redhat/thermostat/storage/query/LiteralSetExpressionTest.java storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/MongoExpressionParser.java storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoExpressionParserTest.java web/common/src/main/java/com/redhat/thermostat/web/common/ExpressionSerializer.java web/common/src/test/java/com/redhat/thermostat/web/common/ExpressionSerializerTest.java
diffstat 15 files changed, 710 insertions(+), 57 deletions(-) [+]
line wrap: on
line diff
--- a/storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryComparisonExpression.java	Fri Jun 21 16:36:21 2013 +0200
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryComparisonExpression.java	Fri Jun 28 12:37:39 2013 -0400
@@ -44,7 +44,8 @@
  * @param <T> - the type parameter of this expression's {@link Key}
  */
 public final class BinaryComparisonExpression<T>
-        extends BinaryExpression<LiteralExpression<Key<T>>, LiteralExpression<T>, BinaryComparisonOperator> {
+        extends BinaryExpression<LiteralExpression<Key<T>>, LiteralExpression<T>, BinaryComparisonOperator> 
+        implements ComparisonExpression {
     
     /**
      * Constructs a {@link BinaryComparisonExpression} whose operands are
--- a/storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryLogicalExpression.java	Fri Jun 21 16:36:21 2013 +0200
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/BinaryLogicalExpression.java	Fri Jun 28 12:37:39 2013 -0400
@@ -43,7 +43,8 @@
  * @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> {
+        extends BinaryExpression<S, T, BinaryLogicalOperator>
+        implements LogicalExpression {
 
     /**
      * Constructs a {@link BinaryLogicalExpression} whose operands are
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/BinarySetMembershipExpression.java	Fri Jun 28 12:37:39 2013 -0400
@@ -0,0 +1,66 @@
+/*
+ * 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 multiple values.
+ * @param <T> - the type parameter of this expression's {@link Key}
+ */
+public final class BinarySetMembershipExpression<T>
+        extends BinaryExpression<LiteralExpression<Key<T>>, LiteralSetExpression<T>, BinarySetMembershipOperator>
+        implements ComparisonExpression {
+    
+    /**
+     * Constructs a {@link BinarySetMembershipExpression} whose operands are
+     * a {@link LiteralExpression} for a {@link Key} and a {@link LiteralSetExpression}
+     * for the set of values 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 BinarySetMembershipExpression(LiteralExpression<Key<T>> leftOperand,
+            BinarySetMembershipOperator operator, LiteralSetExpression<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/BinarySetMembershipOperator.java	Fri Jun 28 12:37:39 2013 -0400
@@ -0,0 +1,49 @@
+/*
+ * 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 BinarySetMembershipExpression}
+ */
+public enum BinarySetMembershipOperator implements BinaryOperator {
+    /** Compares if the left-hand value is equal to any of the
+     *  right-hand values */
+    IN,
+    /** Compares if the left-hand value is not equal to all of the
+     *  right-hand values */
+    NOT_IN,
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/ComparisonExpression.java	Fri Jun 28 12:37:39 2013 -0400
@@ -0,0 +1,41 @@
+/*
+ * 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;
+
+public interface ComparisonExpression extends Expression {
+
+}
\ No newline at end of file
--- a/storage/core/src/main/java/com/redhat/thermostat/storage/query/ExpressionFactory.java	Fri Jun 21 16:36:21 2013 +0200
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/ExpressionFactory.java	Fri Jun 28 12:37:39 2013 -0400
@@ -36,6 +36,8 @@
 
 package com.redhat.thermostat.storage.query;
 
+import java.util.Set;
+
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.Query;
 
@@ -115,12 +117,36 @@
     }
     
     /**
+     * Creates a {@link BinarySetMembershipExpression} comparing if the
+     * provided key has a value equal to any of the provided values.
+     * @param key - {@link Key} whose value to compare against the provided values
+     * @param value - a set of values to compare against the key
+     * @param type - the type of values of stored in the provided set
+     * @return the new comparison expression
+     */
+    public <T> BinarySetMembershipExpression<T> in(Key<T> key, Set<T> values, Class<T> type) {
+        return createSetMembershipExpression(key, values, BinarySetMembershipOperator.IN, type);
+    }
+    
+    /**
+     * Creates a {@link BinarySetMembershipExpression} comparing if the
+     * provided key has a value not equal to all of the provided values.
+     * @param key - {@link Key} whose value to compare against the provided values
+     * @param value - a set of values to compare against the key
+     * @param type - the type of values of stored in the provided set
+     * @return the new comparison expression
+     */
+    public <T> BinarySetMembershipExpression<T> notIn(Key<T> key, Set<T> values, Class<T> type) {
+        return createSetMembershipExpression(key, values, BinarySetMembershipOperator.NOT_IN, type);
+    }
+    
+    /**
      * 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) {
+    public <T extends ComparisonExpression> UnaryLogicalExpression<T> not(T expr) {
         return new UnaryLogicalExpression<>(expr, UnaryLogicalOperator.NOT);
     }
     
@@ -150,4 +176,10 @@
         return new BinaryComparisonExpression<>(new LiteralExpression<>(key), op, new LiteralExpression<>(value));
     }
     
+    private <T> BinarySetMembershipExpression<T> createSetMembershipExpression(Key<T> key, Set<T> values,
+            BinarySetMembershipOperator op, Class<T> type) {
+        return new BinarySetMembershipExpression<>(new LiteralExpression<>(key), op, 
+                new LiteralSetExpression<>(values, type));
+    }
+    
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/LiteralSetExpression.java	Fri Jun 28 12:37:39 2013 -0400
@@ -0,0 +1,101 @@
+/*
+ * 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 java.util.Collections;
+import java.util.Set;
+
+/**
+ * An {@link Expression} that contains a set of values.
+ * @param <T> - the type of this expression's values
+ */
+public final class LiteralSetExpression<T> implements Expression {
+
+    private Set<T> values;
+    private Class<T> type;
+    
+    /**
+     * Constructs a {@link LiteralSetExpression} given a list of values
+     * and its type.
+     * <p>
+     * This constructor exists mainly for JSON serialization, use methods in
+     * {@link ExpressionFactory} instead of this constructor.
+     * @param values - a list of values for this expression
+     * @param type - the type of values of stored in the provided set
+     * (needed for serialization)
+     */
+    public LiteralSetExpression(Set<T> values, Class<T> type) {
+        // Don't need to write, make read-only copy
+        this.values = Collections.unmodifiableSet(values);
+        this.type = type;
+    }
+    
+    /**
+     * @return the values represented by this expression
+     */
+    public Set<T> getValues() {
+        return values;
+    }
+    
+    /**
+     * @return the type of values stored in this expression
+     */
+    public Class<T> getType() {
+        return type;
+    }
+    
+    @Override
+    public boolean equals(Object obj) {
+        boolean result = false;
+        if (obj != null && obj instanceof LiteralSetExpression) {
+            LiteralSetExpression<?> otherExpr = (LiteralSetExpression<?>) obj;
+            result = values.equals(otherExpr.values)
+                    && type.equals(otherExpr.type);
+        }
+        return result;
+    }
+    
+    @Override
+    public int hashCode() {
+        return values.hashCode() + type.hashCode();
+    }
+    
+    @Override
+    public String toString() {
+        return values.toString();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/LogicalExpression.java	Fri Jun 28 12:37:39 2013 -0400
@@ -0,0 +1,41 @@
+/*
+ * 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;
+
+public interface LogicalExpression extends Expression {
+
+}
--- a/storage/core/src/main/java/com/redhat/thermostat/storage/query/UnaryLogicalExpression.java	Fri Jun 21 16:36:21 2013 +0200
+++ b/storage/core/src/main/java/com/redhat/thermostat/storage/query/UnaryLogicalExpression.java	Fri Jun 28 12:37:39 2013 -0400
@@ -38,11 +38,12 @@
 
 /**
  * A {@link UnaryExpression} which represents a boolean formula
- * with one expression and a logical operator.
+ * with one comparison 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> {
+public final class UnaryLogicalExpression<T extends ComparisonExpression> extends
+        UnaryExpression<T, UnaryLogicalOperator>
+        implements LogicalExpression {
 
     /**
      * Constructs a {@link UnaryLogicalExpression} given an operand
@@ -50,9 +51,8 @@
      * <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 operand - the 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);
--- a/storage/core/src/test/java/com/redhat/thermostat/storage/query/ExpressionFactoryTest.java	Fri Jun 21 16:36:21 2013 +0200
+++ b/storage/core/src/test/java/com/redhat/thermostat/storage/query/ExpressionFactoryTest.java	Fri Jun 28 12:37:39 2013 -0400
@@ -36,8 +36,12 @@
 
 package com.redhat.thermostat.storage.query;
 
-import static org.junit.Assert.*;
-import static org.mockito.Mockito.*;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -48,6 +52,7 @@
     
     private static final Key<String> key = new Key<>("hello", true);
     private static final String VALUE = "world";
+    private static final Set<String> VALUES = new HashSet<>(Arrays.asList("world", "worlds"));
     ExpressionFactory factory;
 
     @Before
@@ -104,9 +109,25 @@
     }
 
     @Test
+    public void testIn() {
+        Expression expr = new BinarySetMembershipExpression<>(
+                new LiteralExpression<>(key), BinarySetMembershipOperator.IN,
+                new LiteralSetExpression<>(VALUES, String.class));
+        assertEquals(expr, factory.in(key, VALUES, String.class));
+    }
+    
+    @Test
+    public void testNotIn() {
+        Expression expr = new BinarySetMembershipExpression<>(
+                new LiteralExpression<>(key), BinarySetMembershipOperator.NOT_IN,
+                new LiteralSetExpression<>(VALUES, String.class));
+        assertEquals(expr, factory.notIn(key, VALUES, String.class));
+    }
+    
+    @Test
     public void testNot() {
-        Expression operand = mock(Expression.class);
-        Expression expr = new UnaryLogicalExpression<Expression>(operand, UnaryLogicalOperator.NOT);
+        ComparisonExpression operand = mock(ComparisonExpression.class);
+        Expression expr = new UnaryLogicalExpression<ComparisonExpression>(operand, UnaryLogicalOperator.NOT);
         assertEquals(expr, factory.not(operand));
     }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/test/java/com/redhat/thermostat/storage/query/LiteralSetExpressionTest.java	Fri Jun 28 12:37:39 2013 -0400
@@ -0,0 +1,105 @@
+/*
+ * 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 java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class LiteralSetExpressionTest {
+
+    private static final Set<Object> VALUES = new HashSet<>(Arrays.asList(new Object(), new Object(), new Object()));
+    private LiteralSetExpression<Object> expr;
+
+    @Before
+    public void setup() {
+        expr = new LiteralSetExpression<Object>(VALUES, Object.class);
+    }
+    
+    @Test
+    public void testGetValues() {
+        assertEquals(VALUES, expr.getValues());
+    }
+    
+    @Test(expected=UnsupportedOperationException.class)
+    public void testGetValuesImmutable() {
+        Set<Object> values = expr.getValues();
+        values.add(new Object());
+    }
+    
+    @Test
+    public void testEquals() {
+        LiteralSetExpression<Object> otherExpr = new LiteralSetExpression<Object>(VALUES, Object.class);
+        assertEquals(expr, otherExpr);
+    }
+    
+    @Test
+    public void testDeepEquals() {
+        Set<Object> otherValues = new HashSet<>(VALUES);
+        LiteralSetExpression<Object> otherExpr = new LiteralSetExpression<Object>(otherValues, Object.class);
+        assertEquals(expr, otherExpr);
+    }
+    
+    @Test
+    public void testNotEquals() {
+        Set<Object> otherValue = new HashSet<>(Arrays.asList(new Object(), new Object(), new Object()));
+        LiteralSetExpression<Object> otherExpr = new LiteralSetExpression<Object>(otherValue, Object.class);
+        
+        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(VALUES.hashCode() + Object.class.hashCode(), expr.hashCode());
+    }
+}
--- a/storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/MongoExpressionParser.java	Fri Jun 21 16:36:21 2013 +0200
+++ b/storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/MongoExpressionParser.java	Fri Jun 28 12:37:39 2013 -0400
@@ -47,7 +47,11 @@
 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.BinarySetMembershipExpression;
+import com.redhat.thermostat.storage.query.BinarySetMembershipOperator;
+import com.redhat.thermostat.storage.query.ComparisonExpression;
 import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.LiteralSetExpression;
 import com.redhat.thermostat.storage.query.LiteralExpression;
 import com.redhat.thermostat.storage.query.UnaryLogicalExpression;
 import com.redhat.thermostat.storage.query.UnaryLogicalOperator;
@@ -57,7 +61,10 @@
     public DBObject parse(Expression expr) {
         DBObject result;
         if (expr instanceof BinaryComparisonExpression) {
-            result = parseBinaryLiteralExpression((BinaryComparisonExpression<?>) expr);
+            result = parseBinaryComparisonExpression((BinaryComparisonExpression<?>) expr);
+        }
+        else if (expr instanceof BinarySetMembershipExpression) {
+            result = parseBinarySetComparisonExpression((BinarySetMembershipExpression<?>) expr);
         }
         else if (expr instanceof BinaryLogicalExpression) {
             // LHS OP RHS ==> { OP : [ LHS, RHS ] }
@@ -74,30 +81,19 @@
         }
         else if (expr instanceof UnaryLogicalExpression) {
             UnaryLogicalExpression<?> uniExpr = (UnaryLogicalExpression<?>) expr;
-            Expression operand = uniExpr.getOperand();
+            ComparisonExpression 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) {
+            // Special case
+            if (op == UnaryLogicalOperator.NOT && operand instanceof BinaryComparisonExpression) {
                 BinaryComparisonExpression<?> binExpr = (BinaryComparisonExpression<?>) operand;
                 if (binExpr.getOperator() == BinaryComparisonOperator.EQUALS) {
+                    // TODO Convert to $ne?
                     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");
-            }
+            DBObject object = parse(operand);
+            insertUnaryLogicalOperator(object, op);
+            result = object;
         }
         else {
             throw new IllegalArgumentException("Unknown Expression of type " + expr.getClass());
@@ -106,7 +102,19 @@
         return result;
     }
 
-    private <T> DBObject parseBinaryLiteralExpression(BinaryComparisonExpression<T> expr) {
+    private void insertUnaryLogicalOperator(DBObject object, UnaryLogicalOperator op) {
+        String strOp = getLogicalOperator(op);
+        // 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);
+        }
+    }
+
+    private <T> DBObject parseBinaryComparisonExpression(BinaryComparisonExpression<T> expr) {
         BasicDBObjectBuilder builder = BasicDBObjectBuilder.start();
         LiteralExpression<Key<T>> leftExpr = expr.getLeftOperand();
         LiteralExpression<T> rightExpr = expr.getRightOperand();
@@ -119,7 +127,7 @@
             // LHS OP RHS => { LHS : { OP : RHS } }
             builder.push(leftExpr.getValue().getName());
 
-            String mongoOp = getLiteralOperator(op);
+            String mongoOp = getComparisonOperator(op);
             builder.add(mongoOp, rightExpr.getValue());
 
             builder.pop();
@@ -127,7 +135,22 @@
         return builder.get();
     }
     
-    private String getLiteralOperator(BinaryComparisonOperator operator) {
+    private <T> DBObject parseBinarySetComparisonExpression(BinarySetMembershipExpression<T> expr) {
+        BasicDBObjectBuilder builder = BasicDBObjectBuilder.start();
+        LiteralExpression<Key<T>> leftExpr = expr.getLeftOperand();
+        LiteralSetExpression<T> rightExpr = expr.getRightOperand();
+        BinarySetMembershipOperator op = expr.getOperator();
+        // LHS OP [ RHS ] => { LHS : { OP : [ RHS ] } }
+        builder.push(leftExpr.getValue().getName());
+
+        String mongoOp = getSetMembershipOperator(op);
+        builder.add(mongoOp, rightExpr.getValues());
+
+        builder.pop();
+        return builder.get();
+    }
+    
+    private String getComparisonOperator(BinaryComparisonOperator operator) {
         String result;
         switch (operator) {
         case NOT_EQUAL_TO:
@@ -151,6 +174,21 @@
         return result;
     }
     
+    private String getSetMembershipOperator(BinarySetMembershipOperator operator) {
+        String result;
+        switch (operator) {
+        case IN:
+            result = "$in";
+            break;
+        case NOT_IN:
+            result = "$nin";
+            break;
+        default:
+            throw new IllegalArgumentException("MongoQuery can not handle " + operator);
+        }
+        return result;
+    }
+    
     private String getLogicalOperator(BinaryLogicalOperator operator) {
         String result;
         switch (operator) {
@@ -177,5 +215,5 @@
         }
         return result;
     }
-
+    
 }
--- a/storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoExpressionParserTest.java	Fri Jun 21 16:36:21 2013 +0200
+++ b/storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoExpressionParserTest.java	Fri Jun 28 12:37:39 2013 -0400
@@ -38,6 +38,10 @@
 
 import static org.junit.Assert.assertEquals;
 
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
 import org.junit.Before;
 import org.junit.Test;
 
@@ -55,15 +59,16 @@
     private final Key<String> KEY_3 = new Key<>("key", true);
     
     private MongoExpressionParser parser;
+    private ExpressionFactory factory;
 
     @Before
     public void setUp() throws Exception {
         parser = new MongoExpressionParser();
+        factory = new ExpressionFactory();
     }
 
     @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));
@@ -71,7 +76,6 @@
 
     @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));
@@ -79,7 +83,6 @@
 
     @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));
@@ -87,7 +90,6 @@
 
     @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));
@@ -95,7 +97,6 @@
 
     @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));
@@ -103,15 +104,29 @@
 
     @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 testWhereIn() {
+        Set<String> values = new HashSet<>(Arrays.asList("value", "values"));
+        Expression expr = factory.in(KEY_3, values, String.class);
+        DBObject query = BasicDBObjectBuilder.start().push(KEY_3.getName()).add("$in", values).get();
+        assertEquals(query, parser.parse(expr));
+    }
+    
+    @Test
+    public void testWhereNotIn() {
+        Set<String> values = new HashSet<>(Arrays.asList("value", "values"));
+        Expression expr = factory.notIn(KEY_3, values, String.class);
+        DBObject query = BasicDBObjectBuilder.start().push(KEY_3.getName()).add("$nin", values).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();
@@ -123,7 +138,6 @@
     
     @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();
@@ -135,7 +149,6 @@
     
     @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();
@@ -147,7 +160,6 @@
     
     @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();
@@ -159,7 +171,6 @@
     
     @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();
@@ -170,31 +181,30 @@
     }
     
     @Test
-    public void testWhereNot() {
-        ExpressionFactory factory = new ExpressionFactory();
+    public void testWhereNotCompare() {
         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
+    public void testWhereNotSetCompare() {
+        Set<Integer> values = new HashSet<>(Arrays.asList(1, 2));
+        Expression expr = factory.not(factory.in(KEY_1, values, Integer.class));
+
+        DBObject dbObject = BasicDBObjectBuilder.start().push("test").push("$not").add("$in", values).get();
+        assertEquals(dbObject, 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),
--- a/web/common/src/main/java/com/redhat/thermostat/web/common/ExpressionSerializer.java	Fri Jun 21 16:36:21 2013 +0200
+++ b/web/common/src/main/java/com/redhat/thermostat/web/common/ExpressionSerializer.java	Fri Jun 28 12:37:39 2013 -0400
@@ -37,7 +37,10 @@
 package com.redhat.thermostat.web.common;
 
 import java.lang.reflect.Type;
+import java.util.HashSet;
+import java.util.Set;
 
+import com.google.gson.JsonArray;
 import com.google.gson.JsonDeserializationContext;
 import com.google.gson.JsonDeserializer;
 import com.google.gson.JsonElement;
@@ -50,8 +53,12 @@
 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.BinarySetMembershipExpression;
+import com.redhat.thermostat.storage.query.BinarySetMembershipOperator;
+import com.redhat.thermostat.storage.query.ComparisonExpression;
 import com.redhat.thermostat.storage.query.Expression;
 import com.redhat.thermostat.storage.query.LiteralExpression;
+import com.redhat.thermostat.storage.query.LiteralSetExpression;
 import com.redhat.thermostat.storage.query.Operator;
 import com.redhat.thermostat.storage.query.UnaryLogicalExpression;
 import com.redhat.thermostat.storage.query.UnaryLogicalOperator;
@@ -89,6 +96,9 @@
             if (BinaryComparisonExpression.class.isAssignableFrom(clazz)) {
                 result = deserializeBinaryComparisonExpression(json, context);
             }
+            else if (BinarySetMembershipExpression.class.isAssignableFrom(clazz)) {
+                result = deserializeBinarySetMembershipExpression(json, context);
+            }
             else if (BinaryLogicalExpression.class.isAssignableFrom(clazz)) {
                 result = deserializeBinaryLogicalExpression(json, context);
             }
@@ -98,6 +108,9 @@
             else if (LiteralExpression.class.isAssignableFrom(clazz)) {
                 result = deserializeLiteralExpression(json, context);
             }
+            else if (LiteralSetExpression.class.isAssignableFrom(clazz)) {
+                result = deserializeLiteralArrayExpression(json, context);
+            }
             else {
                 throw new JsonParseException("Unknown Expression of type " + className);
             }
@@ -118,6 +131,18 @@
         BinaryComparisonOperator op = context.deserialize(jsonOp, Operator.class);
         return new BinaryComparisonExpression<>(left, op, right);
     }
+    
+    private <T> Expression deserializeBinarySetMembershipExpression(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);
+        LiteralSetExpression<T> right = context.deserialize(jsonRight, Expression.class);
+        BinarySetMembershipOperator op = context.deserialize(jsonOp, Operator.class);
+        return new BinarySetMembershipExpression<>(left, op, right);
+    }
 
     private <S extends Expression, T extends Expression> Expression deserializeBinaryLogicalExpression(JsonElement json,
             JsonDeserializationContext context) {
@@ -131,7 +156,7 @@
         return new BinaryLogicalExpression<>(left, op, right);
     }
 
-    private <T extends Expression> Expression deserializeUnaryLogicalExpression(JsonElement json,
+    private <T extends ComparisonExpression> Expression deserializeUnaryLogicalExpression(JsonElement json,
             JsonDeserializationContext context) {
         JsonElement jsonOperand = json.getAsJsonObject().get(PROP_OPERAND);
         JsonElement jsonOp = json.getAsJsonObject().get(PROP_OPERATOR);
@@ -149,13 +174,38 @@
         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);
     }
 
+    private Expression deserializeLiteralArrayExpression(JsonElement json,
+            JsonDeserializationContext context) throws ClassNotFoundException {
+        JsonElement jsonValue = json.getAsJsonObject().get(PROP_VALUE);
+        if (jsonValue instanceof JsonArray) {
+            JsonElement jsonValueClass = json.getAsJsonObject().get(PROP_VALUE_CLASS);
+            String valueClassName = jsonValueClass.getAsString();
+            Class<?> type = Class.forName(valueClassName);
+            JsonArray jsonArr = (JsonArray) jsonValue;
+            return makeLiteralArrayExpression(context, jsonArr, type);
+        }
+        else {
+            throw new JsonParseException("No JsonArray supplied for " + PROP_VALUE);
+        }
+    }
+
+    private <T> Expression makeLiteralArrayExpression(JsonDeserializationContext context,
+            JsonArray jsonArr, Class<T> valueClass) {
+        Set<T> values = new HashSet<>();
+        for (JsonElement element : jsonArr) {
+            T value = context.deserialize(element, valueClass);
+            values.add(value);
+        }
+        return new LiteralSetExpression<>(values, valueClass);
+    }
+
     @Override
     public JsonElement serialize(Expression src, Type typeOfSrc,
             JsonSerializationContext context) {
@@ -181,6 +231,16 @@
             result.add(PROP_OPERATOR, op);
             result.add(PROP_OPERAND_RIGHT, right);
         }
+        else if (src instanceof BinarySetMembershipExpression) {
+            BinarySetMembershipExpression<?> binExpr = (BinarySetMembershipExpression<?>) 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());
@@ -197,11 +257,30 @@
             // Store the type of value to properly deserialize it later
             result.addProperty(PROP_VALUE_CLASS, litExpr.getValue().getClass().getCanonicalName());
         }
+        else if (src instanceof LiteralSetExpression) {
+            LiteralSetExpression<?> litArrExpr = (LiteralSetExpression<?>) src;
+            result = serializeLiteralArrayExpression(litArrExpr, context);
+        }
         else {
             throw new JsonParseException("Unknown expression of type " + src.getClass());
         }
         result.addProperty(PROP_CLASS_NAME, src.getClass().getCanonicalName());
         return result;
     }
+    
+    private <T> JsonObject serializeLiteralArrayExpression(LiteralSetExpression<T> expr,
+            JsonSerializationContext context) {
+        JsonObject result = new JsonObject();
+        Set<T> list = expr.getValues();
+        Class<T> type = expr.getType();
+        JsonArray arr = new JsonArray();
+        for (T element : list) {
+            arr.add(context.serialize(element, type));
+        }
+        result.add(PROP_VALUE, arr);
+        // Store type of list elements
+        result.addProperty(PROP_VALUE_CLASS, type.getCanonicalName());
+        return result;
+    }
 
 }
--- a/web/common/src/test/java/com/redhat/thermostat/web/common/ExpressionSerializerTest.java	Fri Jun 21 16:36:21 2013 +0200
+++ b/web/common/src/test/java/com/redhat/thermostat/web/common/ExpressionSerializerTest.java	Fri Jun 28 12:37:39 2013 -0400
@@ -38,18 +38,24 @@
 
 import static org.junit.Assert.assertEquals;
 
+import java.util.Arrays;
+import java.util.HashSet;
+
 import org.junit.Before;
 import org.junit.Test;
 
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
 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.BinarySetMembershipExpression;
 import com.redhat.thermostat.storage.query.Expression;
 import com.redhat.thermostat.storage.query.ExpressionFactory;
+import com.redhat.thermostat.storage.query.LiteralSetExpression;
 import com.redhat.thermostat.storage.query.LiteralExpression;
 import com.redhat.thermostat.storage.query.Operator;
 import com.redhat.thermostat.storage.query.UnaryLogicalExpression;
@@ -85,6 +91,22 @@
     }
     
     @Test
+    public void testDeserializeLiteralArrayExpression() {
+        Expression expr = new LiteralSetExpression<String>(new HashSet<>(Arrays.asList("hello", "goodbye")), 
+                String.class);
+        
+        JsonObject json = new JsonObject();
+        JsonArray arr = new JsonArray();
+        arr.add(gson.toJsonTree("hello"));
+        arr.add(gson.toJsonTree("goodbye"));
+        json.add(ExpressionSerializer.PROP_VALUE, arr);
+        json.addProperty(ExpressionSerializer.PROP_VALUE_CLASS, String.class.getCanonicalName());
+        json.addProperty(ExpressionSerializer.PROP_CLASS_NAME, LiteralSetExpression.class.getCanonicalName());
+        
+        assertEquals(expr, gson.fromJson(json, Expression.class));
+    }
+    
+    @Test
     public void testDeserializeBinaryComparisonExpression() {
         ExpressionFactory factory = new ExpressionFactory();
         BinaryComparisonExpression<String> expr = factory.equalTo(key, "world");
@@ -99,6 +121,21 @@
     }
     
     @Test
+    public void testDeserializeBinarySetMembershipExpression() {
+        ExpressionFactory factory = new ExpressionFactory();
+        BinarySetMembershipExpression<String> expr = factory.in(key, 
+                new HashSet<>(Arrays.asList("world", "goodbye")), String.class);
+        
+        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, BinarySetMembershipExpression.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"));
@@ -153,6 +190,22 @@
     }
     
     @Test
+    public void testSerializeLiteralArrayExpression() {
+        Expression expr = new LiteralSetExpression<String>(new HashSet<>(Arrays.asList("hello", "goodbye")),
+                String.class);
+        
+        JsonObject json = new JsonObject();
+        JsonArray arr = new JsonArray();
+        arr.add(gson.toJsonTree("hello"));
+        arr.add(gson.toJsonTree("goodbye"));
+        json.add(ExpressionSerializer.PROP_VALUE, arr);
+        json.addProperty(ExpressionSerializer.PROP_VALUE_CLASS, String.class.getCanonicalName());
+        json.addProperty(ExpressionSerializer.PROP_CLASS_NAME, LiteralSetExpression.class.getCanonicalName());
+        
+        assertEquals(gson.toJson(json), gson.toJson(expr));
+    }
+    
+    @Test
     public void testSerializeBinaryComparisonExpression() {
         ExpressionFactory factory = new ExpressionFactory();
         BinaryComparisonExpression<String> expr = factory.equalTo(key, "world");
@@ -167,6 +220,21 @@
     }
     
     @Test
+    public void testSerializeBinarySetMembershipExpression() {
+        ExpressionFactory factory = new ExpressionFactory();
+        BinarySetMembershipExpression<String> expr = factory.in(key, new HashSet<>(Arrays.asList("world", "goodbye")),
+                String.class);
+        
+        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, BinarySetMembershipExpression.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"));